diff --git a/.changeset/eighty-planets-sit.md b/.changeset/eighty-planets-sit.md new file mode 100644 index 00000000000..5d060f62798 --- /dev/null +++ b/.changeset/eighty-planets-sit.md @@ -0,0 +1,9 @@ +--- +'@clerk/clerk-js': patch +'@clerk/shared': patch +'@clerk/types': patch +--- + +Introduces a new resource called OrganizationMembership + ++ useOrganization has been updated in order to return a list of domain with the above type diff --git a/packages/clerk-js/src/core/resources/Organization.ts b/packages/clerk-js/src/core/resources/Organization.ts index 3278964a600..801adc79f26 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, + GetMembershipRequestParams, GetMembershipsParams, GetPendingInvitationsParams, InviteMemberParams, @@ -13,6 +14,8 @@ import type { OrganizationInvitationJSON, OrganizationJSON, OrganizationMembershipJSON, + OrganizationMembershipRequestJSON, + OrganizationMembershipRequestResource, OrganizationResource, SetOrganizationLogoParams, UpdateMembershipParams, @@ -23,6 +26,7 @@ import { unixEpochToDate } from '../../utils/date'; import { convertPageToOffset } from '../../utils/pagesToOffset'; import { BaseResource, OrganizationInvitation, OrganizationMembership } from './internal'; import { OrganizationDomain } from './OrganizationDomain'; +import { OrganizationMembershipRequest } from './OrganizationMembershipRequest'; export class Organization extends BaseResource implements OrganizationResource { pathRoot = '/organizations'; @@ -114,6 +118,29 @@ export class Organization extends BaseResource implements OrganizationResource { return new OrganizationDomain(json); }; + getMembershipRequests = async ( + getRequestParam?: GetMembershipRequestParams, + ): Promise> => { + return await BaseResource._fetch({ + path: `/organizations/${this.id}/membership_requests`, + method: 'GET', + search: convertPageToOffset(getRequestParam) as any, + }) + .then(res => { + const { data: requests, total_count } = + res?.response as unknown as ClerkPaginatedResponse; + + return { + total_count, + data: requests.map(request => new OrganizationMembershipRequest(request)), + }; + }) + .catch(() => ({ + total_count: 0, + data: [], + })); + }; + createDomain = async (name: string): Promise => { return OrganizationDomain.create(this.id, { name }); }; diff --git a/packages/clerk-js/src/core/resources/OrganizationMembershipRequest.test.ts b/packages/clerk-js/src/core/resources/OrganizationMembershipRequest.test.ts new file mode 100644 index 00000000000..73cf10b92a1 --- /dev/null +++ b/packages/clerk-js/src/core/resources/OrganizationMembershipRequest.test.ts @@ -0,0 +1,26 @@ +import { OrganizationMembershipRequest } from './internal'; + +describe('OrganizationMembership', () => { + it('has the same initial properties', () => { + const organizationMembershipRequest = new OrganizationMembershipRequest({ + object: 'organization_membership_request', + id: 'test_id', + organization_id: 'test_org_id', + status: 'pending', + created_at: 12345, + updated_at: 5678, + public_user_data: { + object: 'public_user_data', + first_name: 'test_first_name', + last_name: 'test_last_name', + profile_image_url: 'test_url', + image_url: 'https://clerk.com', + identifier: 'test@identifier.gr', + id: 'test_user_id', + has_image: true, + }, + }); + + expect(organizationMembershipRequest).toMatchSnapshot(); + }); +}); diff --git a/packages/clerk-js/src/core/resources/OrganizationMembershipRequest.ts b/packages/clerk-js/src/core/resources/OrganizationMembershipRequest.ts new file mode 100644 index 00000000000..c38b5cd1a3d --- /dev/null +++ b/packages/clerk-js/src/core/resources/OrganizationMembershipRequest.ts @@ -0,0 +1,44 @@ +import type { OrganizationInvitationStatus, OrganizationMembershipRequestResource, PublicUserData } from '@clerk/types'; +import type { OrganizationMembershipRequestJSON } from '@clerk/types'; + +import { BaseResource } from './Base'; + +export class OrganizationMembershipRequest extends BaseResource implements OrganizationMembershipRequestResource { + id!: string; + organizationId!: string; + status!: OrganizationInvitationStatus; + publicUserData!: PublicUserData; + createdAt!: Date; + updatedAt!: Date; + + constructor(data: OrganizationMembershipRequestJSON) { + super(); + this.fromJSON(data); + } + + accept = async (): Promise => { + return await this._basePost({ + path: `/organizations/${this.organizationId}/membership_requests/${this.id}/accept`, + }); + }; + + protected fromJSON(data: OrganizationMembershipRequestJSON | null): this { + if (data) { + this.id = data.id; + this.organizationId = data.organization_id; + this.status = data.status; + if (data.public_user_data) { + this.publicUserData = { + firstName: data.public_user_data.first_name, + lastName: data.public_user_data.last_name, + profileImageUrl: data.public_user_data.profile_image_url, + imageUrl: data.public_user_data.image_url, + hasImage: data.public_user_data.has_image, + identifier: data.public_user_data.identifier, + userId: data.public_user_data.user_id, + }; + } + } + return this; + } +} 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 567c23b6bad..522b944a0cf 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], + "getMembershipRequests": [Function], "getMemberships": [Function], "getPendingInvitations": [Function], "hasImage": true, 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 3f2e3988593..a572c4e82f4 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], + "getMembershipRequests": [Function], "getMemberships": [Function], "getPendingInvitations": [Function], "hasImage": true, 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 new file mode 100644 index 00000000000..a3ec337996c --- /dev/null +++ b/packages/clerk-js/src/core/resources/__snapshots__/OrganizationMembershipRequest.test.ts.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`OrganizationMembership has the same initial properties 1`] = ` +OrganizationMembershipRequest { + "accept": [Function], + "id": "test_id", + "organizationId": "test_org_id", + "pathRoot": "", + "publicUserData": { + "firstName": "test_first_name", + "hasImage": true, + "identifier": "test@identifier.gr", + "imageUrl": "https://clerk.com", + "lastName": "test_last_name", + "profileImageUrl": "test_url", + "userId": undefined, + }, + "status": "pending", +} +`; diff --git a/packages/clerk-js/src/core/resources/internal.ts b/packages/clerk-js/src/core/resources/internal.ts index 89916282c4e..2c074362d68 100644 --- a/packages/clerk-js/src/core/resources/internal.ts +++ b/packages/clerk-js/src/core/resources/internal.ts @@ -16,6 +16,7 @@ export * from './Organization'; export * from './OrganizationDomain'; export * from './OrganizationInvitation'; export * from './OrganizationMembership'; +export * from './OrganizationMembershipRequest'; export * from './SamlAccount'; export * from './Session'; export * from './SessionWithActivities'; diff --git a/packages/shared/src/hooks/useOrganization.tsx b/packages/shared/src/hooks/useOrganization.tsx index 0110e00ac0b..93e08c3e61a 100644 --- a/packages/shared/src/hooks/useOrganization.tsx +++ b/packages/shared/src/hooks/useOrganization.tsx @@ -1,20 +1,21 @@ import type { ClerkPaginationParams, GetDomainsParams, + GetMembershipRequestParams, GetMembershipsParams, GetPendingInvitationsParams, OrganizationDomainResource, OrganizationInvitationResource, + OrganizationMembershipRequestResource, OrganizationMembershipResource, OrganizationResource, } from '@clerk/types'; import type { ClerkPaginatedResponse } from '@clerk/types'; -import { useRef } from 'react'; import useSWR from 'swr'; import { useClerkInstanceContext, useOrganizationContext, useSessionContext } from './contexts'; import type { PaginatedResources, PaginatedResourcesWithDefault } from './types'; -import { usePagesOrInfinite } from './usePagesOrInfinite'; +import { usePagesOrInfinite, useWithSafeValues } from './usePagesOrInfinite'; type UseOrganizationParams = { invitationList?: GetPendingInvitationsParams; @@ -25,6 +26,12 @@ type UseOrganizationParams = { infinite?: boolean; keepPreviousData?: boolean; }); + membershipRequests?: + | true + | (GetMembershipRequestParams & { + infinite?: boolean; + keepPreviousData?: boolean; + }); }; type UseOrganizationReturn = @@ -35,6 +42,7 @@ type UseOrganizationReturn = membershipList: undefined; membership: undefined; domains: PaginatedResourcesWithDefault; + membershipRequests: PaginatedResourcesWithDefault; } | { isLoaded: true; @@ -43,6 +51,7 @@ type UseOrganizationReturn = membershipList: undefined; membership: undefined; domains: PaginatedResourcesWithDefault; + membershipRequests: PaginatedResourcesWithDefault; } | { isLoaded: boolean; @@ -51,63 +60,80 @@ type UseOrganizationReturn = membershipList: OrganizationMembershipResource[] | null | undefined; membership: OrganizationMembershipResource | null | undefined; domains: PaginatedResources | null; + membershipRequests: PaginatedResources | null; }; type UseOrganization = (params?: UseOrganizationParams) => UseOrganizationReturn; +const undefinedPaginatedResource = { + data: undefined, + count: undefined, + isLoading: false, + isFetching: false, + isError: false, + page: undefined, + pageCount: undefined, + fetchPage: undefined, + fetchNext: undefined, + fetchPrevious: undefined, + hasNextPage: false, + hasPreviousPage: false, +} as const; + export const useOrganization: UseOrganization = params => { const { invitationList: invitationListParams, membershipList: membershipListParams, domains: domainListParams, + membershipRequests: membershipRequestsListParams, } = params || {}; const { organization, lastOrganizationMember, lastOrganizationInvitation } = useOrganizationContext(); const session = useSessionContext(); - const shouldUseDefaults = typeof domainListParams === 'boolean' && domainListParams; - - // Cache initialPage and initialPageSize until unmount - const initialPageRef = useRef(shouldUseDefaults ? 1 : domainListParams?.initialPage ?? 1); - const pageSizeRef = useRef(shouldUseDefaults ? 10 : domainListParams?.pageSize ?? 10); + const domainSafeValues = useWithSafeValues(domainListParams, { + initialPage: 1, + pageSize: 10, + keepPreviousData: false, + infinite: false, + }); - const triggerInfinite = shouldUseDefaults ? false : !!domainListParams?.infinite; - const internalKeepPreviousData = shouldUseDefaults ? false : !!domainListParams?.keepPreviousData; + const membershipRequestSafeValues = useWithSafeValues(membershipRequestsListParams, { + initialPage: 1, + pageSize: 10, + status: 'pending', + keepPreviousData: false, + infinite: false, + }); const clerk = useClerkInstanceContext(); const shouldFetch = !!(clerk.loaded && session && organization); - const paginatedParams = + const domainParams = typeof domainListParams === 'undefined' ? undefined : { - initialPage: initialPageRef.current, - pageSize: pageSizeRef.current, + initialPage: domainSafeValues.initialPage, + pageSize: domainSafeValues.pageSize, }; - const { - data: isomorphicData, - count: isomorphicCount, - isLoading: isomorphicIsLoading, - isFetching: isomorphicIsFetching, - isError: isomorphicIsError, - page: isomorphicPage, - pageCount, - fetchPage: isomorphicSetPage, - fetchNext, - fetchPrevious, - hasNextPage, - hasPreviousPage, - unstable__mutate, - } = usePagesOrInfinite>( + const membershipRequestParams = + typeof membershipRequestsListParams === 'undefined' + ? undefined + : { + initialPage: membershipRequestSafeValues.initialPage, + pageSize: membershipRequestSafeValues.pageSize, + }; + + const domains = usePagesOrInfinite>( { - ...paginatedParams, + ...domainParams, }, organization?.getDomains, { - keepPreviousData: internalKeepPreviousData, - infinite: triggerInfinite, - enabled: !!paginatedParams, + keepPreviousData: domainSafeValues.keepPreviousData, + infinite: domainSafeValues.infinite, + enabled: !!domainParams, }, { type: 'domains', @@ -115,6 +141,25 @@ export const useOrganization: UseOrganization = params => { }, ); + const membershipRequests = usePagesOrInfinite< + GetMembershipRequestParams, + ClerkPaginatedResponse + >( + { + ...membershipRequestParams, + }, + organization?.getMembershipRequests, + { + keepPreviousData: membershipRequestSafeValues.keepPreviousData, + infinite: membershipRequestSafeValues.infinite, + enabled: !!membershipRequestParams, + }, + { + type: 'membershipRequests', + 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 @@ -154,20 +199,8 @@ export const useOrganization: UseOrganization = params => { invitationList: undefined, membershipList: undefined, membership: undefined, - domains: { - data: undefined, - count: undefined, - isLoading: false, - isFetching: false, - isError: false, - page: undefined, - pageCount: undefined, - fetchPage: undefined, - fetchNext: undefined, - fetchPrevious: undefined, - hasNextPage: false, - hasPreviousPage: false, - }, + domains: undefinedPaginatedResource, + membershipRequests: undefinedPaginatedResource, }; } @@ -179,6 +212,7 @@ export const useOrganization: UseOrganization = params => { membershipList: null, membership: null, domains: null, + membershipRequests: null, }; } @@ -190,20 +224,8 @@ export const useOrganization: UseOrganization = params => { invitationList: undefined, membershipList: undefined, membership: undefined, - domains: { - data: undefined, - count: undefined, - isLoading: false, - isFetching: false, - isError: false, - page: undefined, - pageCount: undefined, - fetchPage: undefined, - fetchNext: undefined, - fetchPrevious: undefined, - hasNextPage: false, - hasPreviousPage: false, - }, + domains: undefinedPaginatedResource, + membershipRequests: undefinedPaginatedResource, }; } @@ -217,21 +239,8 @@ export const useOrganization: UseOrganization = params => { void mutateMembershipList(); void mutateInvitationList(); }, - domains: { - data: isomorphicData, - count: isomorphicCount, - isLoading: isomorphicIsLoading, - isFetching: isomorphicIsFetching, - isError: isomorphicIsError, - page: isomorphicPage, - pageCount, - fetchPage: isomorphicSetPage, - fetchNext, - fetchPrevious, - hasNextPage, - hasPreviousPage, - unstable__mutate, - }, + domains, + membershipRequests, }; }; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index d44bde26842..9bc8c3e4fd2 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -25,6 +25,7 @@ export * from './organization'; export * from './organizationDomain'; export * from './organizationInvitation'; export * from './organizationMembership'; +export * from './organizationMembershipRequest'; export * from './organizationSettings'; export * from './passwords'; export * from './phoneNumber'; diff --git a/packages/types/src/json.ts b/packages/types/src/json.ts index 30b2ea24fbb..6b708b8264f 100644 --- a/packages/types/src/json.ts +++ b/packages/types/src/json.ts @@ -363,6 +363,16 @@ export interface OrganizationDomainJSON extends ClerkResourceJSON { updated_at: number; } +export interface OrganizationMembershipRequestJSON extends ClerkResourceJSON { + object: 'organization_membership_request'; + id: string; + organization_id: string; + status: OrganizationInvitationStatus; + public_user_data: PublicUserDataJSON; + created_at: number; + updated_at: number; +} + export interface UserOrganizationInvitationJSON extends ClerkResourceJSON { object: 'organization_invitation'; id: string; diff --git a/packages/types/src/organization.ts b/packages/types/src/organization.ts index 328480a8519..375bc520194 100644 --- a/packages/types/src/organization.ts +++ b/packages/types/src/organization.ts @@ -1,7 +1,8 @@ import type { ClerkPaginatedResponse, ClerkPaginationParams } from './api'; import type { OrganizationDomainResource } from './organizationDomain'; -import type { OrganizationInvitationResource } from './organizationInvitation'; +import type { OrganizationInvitationResource, OrganizationInvitationStatus } from './organizationInvitation'; import type { MembershipRole, OrganizationMembershipResource } from './organizationMembership'; +import type { OrganizationMembershipRequestResource } from './organizationMembershipRequest'; import type { ClerkResource } from './resource'; declare global { @@ -45,6 +46,9 @@ export interface OrganizationResource extends ClerkResource { getMemberships: (params?: GetMembershipsParams) => Promise; getPendingInvitations: (params?: GetPendingInvitationsParams) => Promise; getDomains: (params?: GetDomainsParams) => Promise>; + getMembershipRequests: ( + params?: GetMembershipRequestParams, + ) => Promise>; addMember: (params: AddMemberParams) => Promise; inviteMember: (params: InviteMemberParams) => Promise; inviteMembers: (params: InviteMembersParams) => Promise; @@ -72,6 +76,19 @@ export type GetDomainsParams = { pageSize?: number; }; +export type GetMembershipRequestParams = { + /** + * This 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 interface AddMemberParams { userId: string; role: MembershipRole; diff --git a/packages/types/src/organizationMembershipRequest.ts b/packages/types/src/organizationMembershipRequest.ts new file mode 100644 index 00000000000..0be7789970a --- /dev/null +++ b/packages/types/src/organizationMembershipRequest.ts @@ -0,0 +1,14 @@ +import type { OrganizationInvitationStatus } from './organizationInvitation'; +import type { ClerkResource } from './resource'; +import type { PublicUserData } from './session'; + +export interface OrganizationMembershipRequestResource extends ClerkResource { + id: string; + organizationId: string; + status: OrganizationInvitationStatus; + publicUserData: PublicUserData; + createdAt: Date; + updatedAt: Date; + + accept: () => Promise; +}