Skip to content

Commit

Permalink
feat(types,clerk-js): Introduce OrganizationSuggestion resource and t…
Browse files Browse the repository at this point in the history
…ypes

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
  • Loading branch information
chanioxaris committed Aug 11, 2023
1 parent 8d1e7d7 commit a412a50
Show file tree
Hide file tree
Showing 11 changed files with 247 additions and 38 deletions.
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

0 comments on commit a412a50

Please sign in to comment.