Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/orange-taxis-eat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/clerk-js': minor
'@clerk/types': minor
---

Introduce a new resource called OrganizationSuggestion along with retrieve() & accept() methods
Also make available the user's suggestions from the useOrganizationList hook
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { OrganizationSuggestion } from './internal';

describe('OrganizationSuggestion', () => {
it('has the same initial properties', () => {
const organizationSuggestion = new OrganizationSuggestion({
object: 'organization_suggestion',
id: 'test_id',
public_organization_data: {
id: 'test_org_id',
name: 'Test org',
slug: 'test-org',
image_url: 'test_image_url',
has_image: true,
},
status: 'pending',
created_at: 12345,
updated_at: 5678,
});

expect(organizationSuggestion).toMatchSnapshot();
});
});
71 changes: 71 additions & 0 deletions packages/clerk-js/src/core/resources/OrganizationSuggestion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type {
ClerkPaginatedResponse,
GetUserOrganizationSuggestionsParams,
OrganizationSuggestionJSON,
OrganizationSuggestionResource,
OrganizationSuggestionStatus,
UserOrganizationInvitationResource,
} from '@clerk/types';

import { unixEpochToDate } from '../../utils/date';
import { convertPageToOffset } from '../../utils/pagesToOffset';
import { BaseResource } from './Base';

export class OrganizationSuggestion extends BaseResource implements OrganizationSuggestionResource {
id!: string;
publicOrganizationData!: UserOrganizationInvitationResource['publicOrganizationData'];
status!: OrganizationSuggestionStatus;
createdAt!: Date;
updatedAt!: Date;

constructor(data: OrganizationSuggestionJSON) {
super();
this.fromJSON(data);
}

static async retrieve(
params?: GetUserOrganizationSuggestionsParams,
): Promise<ClerkPaginatedResponse<OrganizationSuggestion>> {
return await BaseResource._fetch({
path: '/me/organization_suggestions',
method: 'GET',
search: convertPageToOffset(params) as any,
})
.then(res => {
const { data: suggestions, total_count } =
res?.response as unknown as ClerkPaginatedResponse<OrganizationSuggestionJSON>;

return {
total_count,
data: suggestions.map(suggestion => new OrganizationSuggestion(suggestion)),
};
})
.catch(() => ({
total_count: 0,
data: [],
}));
}

accept = async (): Promise<OrganizationSuggestionResource> => {
return await this._basePost({
path: `/me/organization_suggestions/${this.id}/accept`,
});
};

protected fromJSON(data: OrganizationSuggestionJSON | null): this {
if (data) {
this.id = data.id;
this.status = data.status;
this.publicOrganizationData = {
hasImage: data.public_organization_data.has_image,
imageUrl: data.public_organization_data.image_url,
name: data.public_organization_data.name,
id: data.public_organization_data.id,
slug: data.public_organization_data.slug,
};
this.createdAt = unixEpochToDate(data.created_at);
this.updatedAt = unixEpochToDate(data.updated_at);
}
return this;
}
}
6 changes: 6 additions & 0 deletions packages/clerk-js/src/core/resources/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
ExternalAccountJSON,
ExternalAccountResource,
GetUserOrganizationInvitationsParams,
GetUserOrganizationSuggestionsParams,
ImageResource,
OrganizationMembershipResource,
PhoneNumberResource,
Expand Down Expand Up @@ -39,6 +40,7 @@ import {
ExternalAccount,
Image,
OrganizationMembership,
OrganizationSuggestion,
PhoneNumber,
SamlAccount,
SessionWithActivities,
Expand Down Expand Up @@ -260,6 +262,10 @@ export class User extends BaseResource implements UserResource {
return UserOrganizationInvitation.retrieve(params);
};

getOrganizationSuggestions = (params?: GetUserOrganizationSuggestionsParams) => {
return OrganizationSuggestion.retrieve(params);
};

getOrganizationMemberships = async (
retrieveMembership: RetrieveMembershipsParams,
): Promise<OrganizationMembership[]> => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`OrganizationSuggestion has the same initial properties 1`] = `
OrganizationSuggestion {
"accept": [Function],
"createdAt": 1970-01-01T00:00:12.345Z,
"id": "test_id",
"pathRoot": "",
"publicOrganizationData": {
"hasImage": true,
"id": "test_org_id",
"imageUrl": "test_image_url",
"name": "Test org",
"slug": "test-org",
},
"status": "pending",
"updatedAt": 1970-01-01T00:00:05.678Z,
}
`;
1 change: 1 addition & 0 deletions packages/clerk-js/src/core/resources/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export * from './OrganizationDomain';
export * from './OrganizationInvitation';
export * from './OrganizationMembership';
export * from './OrganizationMembershipRequest';
export * from './OrganizationSuggestion';
export * from './SamlAccount';
export * from './Session';
export * from './SessionWithActivities';
Expand Down
96 changes: 65 additions & 31 deletions packages/shared/src/hooks/useOrganizationList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import type {
ClerkPaginatedResponse,
CreateOrganizationParams,
GetUserOrganizationInvitationsParams,
GetUserOrganizationSuggestionsParams,
OrganizationMembershipResource,
OrganizationResource,
OrganizationSuggestionResource,
SetActive,
UserOrganizationInvitationResource,
} from '@clerk/types';
Expand All @@ -19,6 +21,12 @@ type UseOrganizationListParams = {
infinite?: boolean;
keepPreviousData?: boolean;
});
userSuggestions?:
| true
| (GetUserOrganizationSuggestionsParams & {
infinite?: boolean;
keepPreviousData?: boolean;
});
};

type OrganizationList = ReturnType<typeof createOrganizationList>;
Expand All @@ -30,19 +38,21 @@ type UseOrganizationListReturn =
createOrganization: undefined;
setActive: undefined;
userInvitations: PaginatedResourcesWithDefault<UserOrganizationInvitationResource>;
userSuggestions: PaginatedResourcesWithDefault<OrganizationSuggestionResource>;
}
| {
isLoaded: boolean;
organizationList: OrganizationList;
createOrganization: (params: CreateOrganizationParams) => Promise<OrganizationResource>;
setActive: SetActive;
userInvitations: PaginatedResources<UserOrganizationInvitationResource>;
userSuggestions: PaginatedResources<OrganizationSuggestionResource>;
};

type UseOrganizationList = (params?: UseOrganizationListParams) => UseOrganizationListReturn;

export const useOrganizationList: UseOrganizationList = params => {
const { userInvitations } = params || {};
const { userInvitations, userSuggestions } = params || {};

const userInvitationsSafeValues = useWithSafeValues(userInvitations, {
initialPage: 1,
Expand All @@ -52,6 +62,14 @@ export const useOrganizationList: UseOrganizationList = params => {
infinite: false,
});

const userSuggestionsSafeValues = useWithSafeValues(userSuggestions, {
initialPage: 1,
pageSize: 10,
status: 'pending',
keepPreviousData: false,
infinite: false,
});

const clerk = useClerkInstanceContext();
const user = useUserContext();

Expand All @@ -64,23 +82,18 @@ export const useOrganizationList: UseOrganizationList = params => {
status: userInvitationsSafeValues.status,
};

const userSuggestionsParams =
typeof userSuggestions === 'undefined'
? undefined
: {
initialPage: userSuggestionsSafeValues.initialPage,
pageSize: userSuggestionsSafeValues.pageSize,
status: userSuggestionsSafeValues.status,
};

const isClerkLoaded = !!(clerk.loaded && user);

const {
data: isomorphicData,
count: isomorphicCount,
isLoading: isomorphicIsLoading,
isFetching: isomorphicIsFetching,
isError: isomorphicIsError,
page: isomorphicPage,
pageCount,
fetchPage: isomorphicSetPage,
fetchNext,
fetchPrevious,
hasNextPage,
hasPreviousPage,
unstable__mutate,
} = usePagesOrInfinite<
const invitations = usePagesOrInfinite<
GetUserOrganizationInvitationsParams,
ClerkPaginatedResponse<UserOrganizationInvitationResource>
>(
Expand All @@ -99,6 +112,25 @@ export const useOrganizationList: UseOrganizationList = params => {
},
);

const suggestions = usePagesOrInfinite<
GetUserOrganizationSuggestionsParams,
ClerkPaginatedResponse<OrganizationSuggestionResource>
>(
{
...userSuggestionsParams,
},
user?.getOrganizationSuggestions,
{
keepPreviousData: userSuggestionsSafeValues.keepPreviousData,
infinite: userSuggestionsSafeValues.infinite,
enabled: !!userSuggestionsParams,
},
{
type: 'userSuggestions',
userId: user?.id,
},
);

// TODO: Properly check for SSR user values
if (!isClerkLoaded) {
return {
Expand All @@ -121,6 +153,21 @@ export const useOrganizationList: UseOrganizationList = params => {
hasPreviousPage: false,
unstable__mutate: undefined,
},
userSuggestions: {
data: undefined,
count: undefined,
isLoading: false,
isFetching: false,
isError: false,
page: undefined,
pageCount: undefined,
fetchPage: undefined,
fetchNext: undefined,
fetchPrevious: undefined,
hasNextPage: false,
hasPreviousPage: false,
unstable__mutate: undefined,
},
};
}

Expand All @@ -129,21 +176,8 @@ export const useOrganizationList: UseOrganizationList = params => {
organizationList: createOrganizationList(user.organizationMemberships),
setActive: clerk.setActive,
createOrganization: clerk.createOrganization,
userInvitations: {
data: isomorphicData,
count: isomorphicCount,
isLoading: isomorphicIsLoading,
isFetching: isomorphicIsFetching,
isError: isomorphicIsError,
page: isomorphicPage,
pageCount,
fetchPage: isomorphicSetPage,
fetchNext,
fetchPrevious,
hasNextPage,
hasPreviousPage,
unstable__mutate,
},
userInvitations: invitations,
userSuggestions: suggestions,
};
};

Expand Down
1 change: 1 addition & 0 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export * from './organizationInvitation';
export * from './organizationMembership';
export * from './organizationMembershipRequest';
export * from './organizationSettings';
export * from './organizationSuggestion';
export * from './passwords';
export * from './phoneNumber';
export * from './redirects';
Expand Down
26 changes: 19 additions & 7 deletions packages/types/src/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { OrganizationDomainVerificationStatus, OrganizationEnrollmentMode }
import type { OrganizationInvitationStatus } from './organizationInvitation';
import type { MembershipRole } from './organizationMembership';
import type { OrganizationSettingsJSON } from './organizationSettings';
import type { OrganizationSuggestionStatus } from './organizationSuggestion';
import type { SamlIdpSlug } from './saml';
import type { SessionStatus } from './session';
import type { SignInFirstFactor, SignInJSON, SignInSecondFactor } from './signIn';
Expand Down Expand Up @@ -363,6 +364,23 @@ export interface OrganizationDomainJSON extends ClerkResourceJSON {
updated_at: number;
}

export interface PublicOrganizationDataJSON extends ClerkResourceJSON {
id: string;
name: string;
slug: string | null;
has_image: boolean;
image_url: string;
}

export interface OrganizationSuggestionJSON extends ClerkResourceJSON {
object: 'organization_suggestion';
id: string;
public_organization_data: PublicOrganizationDataJSON;
status: OrganizationSuggestionStatus;
created_at: number;
updated_at: number;
}

export interface OrganizationMembershipRequestJSON extends ClerkResourceJSON {
object: 'organization_membership_request';
id: string;
Expand All @@ -377,13 +395,7 @@ export interface UserOrganizationInvitationJSON extends ClerkResourceJSON {
object: 'organization_invitation';
id: string;
email_address: string;
public_organization_data: {
id: string;
name: string;
slug: string | null;
has_image: boolean;
image_url: string;
};
public_organization_data: PublicOrganizationDataJSON;
public_metadata: OrganizationInvitationPublicMetadata;
status: OrganizationInvitationStatus;
role: MembershipRole;
Expand Down
Loading