Skip to content

Commit a412a50

Browse files
committed
feat(types,clerk-js): Introduce OrganizationSuggestion resource and types
We introduce a new OrganizationSuggestion resource and the corresponding types, in order to preview user with suggestions of organizations they can join. The available methods for the time being are retrieve() which return a list of organization suggestions of the user and accept() which allows a user to request to join the organization. Also make available the user's suggestions from the useOrganizationList hook
1 parent 8d1e7d7 commit a412a50

11 files changed

+247
-38
lines changed

.changeset/orange-taxis-eat.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@clerk/clerk-js': minor
3+
'@clerk/types': minor
4+
---
5+
6+
Introduce a new resource called OrganizationSuggestion along with retrieve() & accept() methods
7+
Also make available the user's suggestions from the useOrganizationList hook
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { OrganizationSuggestion } from './internal';
2+
3+
describe('OrganizationSuggestion', () => {
4+
it('has the same initial properties', () => {
5+
const organizationSuggestion = new OrganizationSuggestion({
6+
object: 'organization_suggestion',
7+
id: 'test_id',
8+
public_organization_data: {
9+
id: 'test_org_id',
10+
name: 'Test org',
11+
slug: 'test-org',
12+
image_url: 'test_image_url',
13+
has_image: true,
14+
},
15+
status: 'pending',
16+
created_at: 12345,
17+
updated_at: 5678,
18+
});
19+
20+
expect(organizationSuggestion).toMatchSnapshot();
21+
});
22+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import type {
2+
ClerkPaginatedResponse,
3+
GetUserOrganizationSuggestionsParams,
4+
OrganizationSuggestionJSON,
5+
OrganizationSuggestionResource,
6+
OrganizationSuggestionStatus,
7+
UserOrganizationInvitationResource,
8+
} from '@clerk/types';
9+
10+
import { unixEpochToDate } from '../../utils/date';
11+
import { convertPageToOffset } from '../../utils/pagesToOffset';
12+
import { BaseResource } from './Base';
13+
14+
export class OrganizationSuggestion extends BaseResource implements OrganizationSuggestionResource {
15+
id!: string;
16+
publicOrganizationData!: UserOrganizationInvitationResource['publicOrganizationData'];
17+
status!: OrganizationSuggestionStatus;
18+
createdAt!: Date;
19+
updatedAt!: Date;
20+
21+
constructor(data: OrganizationSuggestionJSON) {
22+
super();
23+
this.fromJSON(data);
24+
}
25+
26+
static async retrieve(
27+
params?: GetUserOrganizationSuggestionsParams,
28+
): Promise<ClerkPaginatedResponse<OrganizationSuggestion>> {
29+
return await BaseResource._fetch({
30+
path: '/me/organization_suggestions',
31+
method: 'GET',
32+
search: convertPageToOffset(params) as any,
33+
})
34+
.then(res => {
35+
const { data: suggestions, total_count } =
36+
res?.response as unknown as ClerkPaginatedResponse<OrganizationSuggestionJSON>;
37+
38+
return {
39+
total_count,
40+
data: suggestions.map(suggestion => new OrganizationSuggestion(suggestion)),
41+
};
42+
})
43+
.catch(() => ({
44+
total_count: 0,
45+
data: [],
46+
}));
47+
}
48+
49+
accept = async (): Promise<OrganizationSuggestionResource> => {
50+
return await this._basePost({
51+
path: `/me/organization_suggestions/${this.id}/accept`,
52+
});
53+
};
54+
55+
protected fromJSON(data: OrganizationSuggestionJSON | null): this {
56+
if (data) {
57+
this.id = data.id;
58+
this.status = data.status;
59+
this.publicOrganizationData = {
60+
hasImage: data.public_organization_data.has_image,
61+
imageUrl: data.public_organization_data.image_url,
62+
name: data.public_organization_data.name,
63+
id: data.public_organization_data.id,
64+
slug: data.public_organization_data.slug,
65+
};
66+
this.createdAt = unixEpochToDate(data.created_at);
67+
this.updatedAt = unixEpochToDate(data.updated_at);
68+
}
69+
return this;
70+
}
71+
}

packages/clerk-js/src/core/resources/User.ts

+6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
ExternalAccountJSON,
1212
ExternalAccountResource,
1313
GetUserOrganizationInvitationsParams,
14+
GetUserOrganizationSuggestionsParams,
1415
ImageResource,
1516
OrganizationMembershipResource,
1617
PhoneNumberResource,
@@ -39,6 +40,7 @@ import {
3940
ExternalAccount,
4041
Image,
4142
OrganizationMembership,
43+
OrganizationSuggestion,
4244
PhoneNumber,
4345
SamlAccount,
4446
SessionWithActivities,
@@ -260,6 +262,10 @@ export class User extends BaseResource implements UserResource {
260262
return UserOrganizationInvitation.retrieve(params);
261263
};
262264

265+
getOrganizationSuggestions = (params?: GetUserOrganizationSuggestionsParams) => {
266+
return OrganizationSuggestion.retrieve(params);
267+
};
268+
263269
getOrganizationMemberships = async (
264270
retrieveMembership: RetrieveMembershipsParams,
265271
): Promise<OrganizationMembership[]> => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`OrganizationSuggestion has the same initial properties 1`] = `
4+
OrganizationSuggestion {
5+
"accept": [Function],
6+
"createdAt": 1970-01-01T00:00:12.345Z,
7+
"id": "test_id",
8+
"pathRoot": "",
9+
"publicOrganizationData": {
10+
"hasImage": true,
11+
"id": "test_org_id",
12+
"imageUrl": "test_image_url",
13+
"name": "Test org",
14+
"slug": "test-org",
15+
},
16+
"status": "pending",
17+
"updatedAt": 1970-01-01T00:00:05.678Z,
18+
}
19+
`;

packages/clerk-js/src/core/resources/internal.ts

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export * from './OrganizationDomain';
1717
export * from './OrganizationInvitation';
1818
export * from './OrganizationMembership';
1919
export * from './OrganizationMembershipRequest';
20+
export * from './OrganizationSuggestion';
2021
export * from './SamlAccount';
2122
export * from './Session';
2223
export * from './SessionWithActivities';

packages/shared/src/hooks/useOrganizationList.tsx

+65-31
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import type {
22
ClerkPaginatedResponse,
33
CreateOrganizationParams,
44
GetUserOrganizationInvitationsParams,
5+
GetUserOrganizationSuggestionsParams,
56
OrganizationMembershipResource,
67
OrganizationResource,
8+
OrganizationSuggestionResource,
79
SetActive,
810
UserOrganizationInvitationResource,
911
} from '@clerk/types';
@@ -19,6 +21,12 @@ type UseOrganizationListParams = {
1921
infinite?: boolean;
2022
keepPreviousData?: boolean;
2123
});
24+
userSuggestions?:
25+
| true
26+
| (GetUserOrganizationSuggestionsParams & {
27+
infinite?: boolean;
28+
keepPreviousData?: boolean;
29+
});
2230
};
2331

2432
type OrganizationList = ReturnType<typeof createOrganizationList>;
@@ -30,19 +38,21 @@ type UseOrganizationListReturn =
3038
createOrganization: undefined;
3139
setActive: undefined;
3240
userInvitations: PaginatedResourcesWithDefault<UserOrganizationInvitationResource>;
41+
userSuggestions: PaginatedResourcesWithDefault<OrganizationSuggestionResource>;
3342
}
3443
| {
3544
isLoaded: boolean;
3645
organizationList: OrganizationList;
3746
createOrganization: (params: CreateOrganizationParams) => Promise<OrganizationResource>;
3847
setActive: SetActive;
3948
userInvitations: PaginatedResources<UserOrganizationInvitationResource>;
49+
userSuggestions: PaginatedResources<OrganizationSuggestionResource>;
4050
};
4151

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

4454
export const useOrganizationList: UseOrganizationList = params => {
45-
const { userInvitations } = params || {};
55+
const { userInvitations, userSuggestions } = params || {};
4656

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

65+
const userSuggestionsSafeValues = useWithSafeValues(userSuggestions, {
66+
initialPage: 1,
67+
pageSize: 10,
68+
status: 'pending',
69+
keepPreviousData: false,
70+
infinite: false,
71+
});
72+
5573
const clerk = useClerkInstanceContext();
5674
const user = useUserContext();
5775

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

85+
const userSuggestionsParams =
86+
typeof userSuggestions === 'undefined'
87+
? undefined
88+
: {
89+
initialPage: userSuggestionsSafeValues.initialPage,
90+
pageSize: userSuggestionsSafeValues.pageSize,
91+
status: userSuggestionsSafeValues.status,
92+
};
93+
6794
const isClerkLoaded = !!(clerk.loaded && user);
6895

69-
const {
70-
data: isomorphicData,
71-
count: isomorphicCount,
72-
isLoading: isomorphicIsLoading,
73-
isFetching: isomorphicIsFetching,
74-
isError: isomorphicIsError,
75-
page: isomorphicPage,
76-
pageCount,
77-
fetchPage: isomorphicSetPage,
78-
fetchNext,
79-
fetchPrevious,
80-
hasNextPage,
81-
hasPreviousPage,
82-
unstable__mutate,
83-
} = usePagesOrInfinite<
96+
const invitations = usePagesOrInfinite<
8497
GetUserOrganizationInvitationsParams,
8598
ClerkPaginatedResponse<UserOrganizationInvitationResource>
8699
>(
@@ -99,6 +112,25 @@ export const useOrganizationList: UseOrganizationList = params => {
99112
},
100113
);
101114

115+
const suggestions = usePagesOrInfinite<
116+
GetUserOrganizationSuggestionsParams,
117+
ClerkPaginatedResponse<OrganizationSuggestionResource>
118+
>(
119+
{
120+
...userSuggestionsParams,
121+
},
122+
user?.getOrganizationSuggestions,
123+
{
124+
keepPreviousData: userSuggestionsSafeValues.keepPreviousData,
125+
infinite: userSuggestionsSafeValues.infinite,
126+
enabled: !!userSuggestionsParams,
127+
},
128+
{
129+
type: 'userSuggestions',
130+
userId: user?.id,
131+
},
132+
);
133+
102134
// TODO: Properly check for SSR user values
103135
if (!isClerkLoaded) {
104136
return {
@@ -121,6 +153,21 @@ export const useOrganizationList: UseOrganizationList = params => {
121153
hasPreviousPage: false,
122154
unstable__mutate: undefined,
123155
},
156+
userSuggestions: {
157+
data: undefined,
158+
count: undefined,
159+
isLoading: false,
160+
isFetching: false,
161+
isError: false,
162+
page: undefined,
163+
pageCount: undefined,
164+
fetchPage: undefined,
165+
fetchNext: undefined,
166+
fetchPrevious: undefined,
167+
hasNextPage: false,
168+
hasPreviousPage: false,
169+
unstable__mutate: undefined,
170+
},
124171
};
125172
}
126173

@@ -129,21 +176,8 @@ export const useOrganizationList: UseOrganizationList = params => {
129176
organizationList: createOrganizationList(user.organizationMemberships),
130177
setActive: clerk.setActive,
131178
createOrganization: clerk.createOrganization,
132-
userInvitations: {
133-
data: isomorphicData,
134-
count: isomorphicCount,
135-
isLoading: isomorphicIsLoading,
136-
isFetching: isomorphicIsFetching,
137-
isError: isomorphicIsError,
138-
page: isomorphicPage,
139-
pageCount,
140-
fetchPage: isomorphicSetPage,
141-
fetchNext,
142-
fetchPrevious,
143-
hasNextPage,
144-
hasPreviousPage,
145-
unstable__mutate,
146-
},
179+
userInvitations: invitations,
180+
userSuggestions: suggestions,
147181
};
148182
};
149183

packages/types/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export * from './organizationInvitation';
2727
export * from './organizationMembership';
2828
export * from './organizationMembershipRequest';
2929
export * from './organizationSettings';
30+
export * from './organizationSuggestion';
3031
export * from './passwords';
3132
export * from './phoneNumber';
3233
export * from './redirects';

packages/types/src/json.ts

+19-7
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { OrganizationDomainVerificationStatus, OrganizationEnrollmentMode }
1010
import type { OrganizationInvitationStatus } from './organizationInvitation';
1111
import type { MembershipRole } from './organizationMembership';
1212
import type { OrganizationSettingsJSON } from './organizationSettings';
13+
import type { OrganizationSuggestionStatus } from './organizationSuggestion';
1314
import type { SamlIdpSlug } from './saml';
1415
import type { SessionStatus } from './session';
1516
import type { SignInFirstFactor, SignInJSON, SignInSecondFactor } from './signIn';
@@ -363,6 +364,23 @@ export interface OrganizationDomainJSON extends ClerkResourceJSON {
363364
updated_at: number;
364365
}
365366

367+
export interface PublicOrganizationDataJSON extends ClerkResourceJSON {
368+
id: string;
369+
name: string;
370+
slug: string | null;
371+
has_image: boolean;
372+
image_url: string;
373+
}
374+
375+
export interface OrganizationSuggestionJSON extends ClerkResourceJSON {
376+
object: 'organization_suggestion';
377+
id: string;
378+
public_organization_data: PublicOrganizationDataJSON;
379+
status: OrganizationSuggestionStatus;
380+
created_at: number;
381+
updated_at: number;
382+
}
383+
366384
export interface OrganizationMembershipRequestJSON extends ClerkResourceJSON {
367385
object: 'organization_membership_request';
368386
id: string;
@@ -377,13 +395,7 @@ export interface UserOrganizationInvitationJSON extends ClerkResourceJSON {
377395
object: 'organization_invitation';
378396
id: string;
379397
email_address: string;
380-
public_organization_data: {
381-
id: string;
382-
name: string;
383-
slug: string | null;
384-
has_image: boolean;
385-
image_url: string;
386-
};
398+
public_organization_data: PublicOrganizationDataJSON;
387399
public_metadata: OrganizationInvitationPublicMetadata;
388400
status: OrganizationInvitationStatus;
389401
role: MembershipRole;

0 commit comments

Comments
 (0)