Skip to content

Commit

Permalink
feat(shared): Organization hooks with setData and revalidate (#1745)
Browse files Browse the repository at this point in the history
* feat(shared): Expose stable mutate

* chore(clerk-js): OrganizationList and switcher with stable mutate

* chore(clerk-js): OrganizationProfile with stable mutate

* fix(shared): Replace mutate with setCache and revalidate

* fix(clerk-js): Replace mutate with setCache and revalidate

* chore(clerk-js,shared): Replace setCache with setData

* fix(shared): Remove unnecessary complexity in types

* chore(clerk-js): Update after rebase

* chore(shared): Add comments with context

* chore(shared): Update changeset
  • Loading branch information
panteliselef authored Nov 2, 2023
1 parent 7a59275 commit cf7ead7
Show file tree
Hide file tree
Showing 13 changed files with 156 additions and 104 deletions.
6 changes: 6 additions & 0 deletions .changeset/blue-ghosts-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/shared': minor
---

Expose `revalidate` and `setData` for paginated lists of data in organization hooks.
`const {userMemberships:{revalidate, setData}} = useOrganizationList({userMemberships:true})`
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useCoreClerk, useCoreOrganizationList } from '../../contexts';
import { localizationKeys } from '../../customizables';
import { useCardState, withCardStateProvider } from '../../elements';
import { handleError } from '../../utils';
import { updateCacheInPlace } from '../OrganizationSwitcher/utils';
import { populateCacheUpdateItem } from '../OrganizationSwitcher/utils';
import { PreviewListItem, PreviewListItemButton } from './shared';
import { MembershipPreview } from './UserMembershipList';
import { organizationListParams } from './utils';
Expand Down Expand Up @@ -39,7 +39,7 @@ export const InvitationPreview = withCardStateProvider((props: UserOrganizationI
})
.then(([updatedItem, organization]) => {
// Update cache in case another listener depends on it
updateCacheInPlace(userInvitations)(updatedItem);
void userInvitations?.setData?.(cachedPages => populateCacheUpdateItem(updatedItem, cachedPages));
setAcceptedOrganization(organization);
})
.catch(err => handleError(err, [], card.setError));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useCoreOrganizationList } from '../../contexts';
import { localizationKeys, Text } from '../../customizables';
import { useCardState, withCardStateProvider } from '../../elements';
import { handleError } from '../../utils';
import { updateCacheInPlace } from '../OrganizationSwitcher/utils';
import { populateCacheUpdateItem } from '../OrganizationSwitcher/utils';
import { PreviewListItem, PreviewListItemButton } from './shared';
import { organizationListParams } from './utils';

Expand All @@ -17,7 +17,7 @@ export const AcceptRejectInvitationButtons = (props: OrganizationSuggestionResou
const handleAccept = () => {
return card
.runAsync(props.accept)
.then(updateCacheInPlace(userSuggestions))
.then(updatedItem => userSuggestions?.setData?.(pages => populateCacheUpdateItem(updatedItem, pages)))
.catch(err => handleError(err, [], card.setError));
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,18 @@ import { DataTable, RoleSelect, RowContainer } from './MemberListTable';

export const ActiveMembersList = () => {
const card = useCardState();
const { organization, memberships, ...rest } = useCoreOrganization({
const { organization, memberships } = useCoreOrganization({
memberships: 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 handleRoleChange = (membership: OrganizationMembershipResource) => (newRole: MembershipRole) => {
return card
.runAsync(async () => {
await membership.update({ role: newRole });
return await membership.update({ role: newRole });
})
.catch(err => handleError(err, [], card.setError));
};
Expand All @@ -36,9 +29,9 @@ export const ActiveMembersList = () => {
return card
.runAsync(async () => {
const destroyedMembership = await membership.destroy();
await memberships?.revalidate?.();
return destroyedMembership;
})
.then(mutateSwrState)
.catch(err => handleError(err, [], card.setError));
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const RemoveDomainPage = () => {
successMessage={localizationKeys('organizationProfile.removeDomainPage.successMessage', {
domain: ref.current?.name,
})}
deleteResource={() => domain?.delete().then(() => (domains as any).unstable__mutate())}
deleteResource={() => domain?.delete().then(() => domains?.revalidate?.())}
breadcrumbTitle={localizationKeys('organizationProfile.profilePage.domainSection.title')}
Breadcrumbs={OrganizationProfileBreadcrumbs}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,23 +49,29 @@ const RequestRow = withCardStateProvider(
(props: { request: OrganizationMembershipRequestResource; onError: ReturnType<typeof useCardState>['setError'] }) => {
const { request, onError } = props;
const card = useCardState();
const { membershipRequests } = useCoreOrganization({
const { membership, membershipRequests } = useCoreOrganization({
membershipRequests: membershipRequestsParams,
});

const onAccept = () => {
if (!membership || !membershipRequests) {
return;
}
return card
.runAsync(async () => {
await request.accept();
await (membershipRequests as any).unstable__mutate?.();
await membershipRequests.revalidate();
}, 'accept')
.catch(err => handleError(err, [], onError));
};
const onReject = () => {
if (!membership || !membershipRequests) {
return;
}
return card
.runAsync(async () => {
await request.reject();
await (membershipRequests as any).unstable__mutate?.();
await membershipRequests.revalidate();
}, 'reject')
.catch(err => handleError(err, [], onError));
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export const VerifiedDomainPage = withCardStateProvider(() => {
const card = useCardState();
const { organizationSettings } = useEnvironment();

const { organization, domains } = useCoreOrganization({
const { membership, organization, domains } = useCoreOrganization({
domains: {
infinite: true,
},
Expand Down Expand Up @@ -147,7 +147,7 @@ export const VerifiedDomainPage = withCardStateProvider(() => {
});

const updateEnrollmentMode = async () => {
if (!domain || !organization) {
if (!domain || !organization || !membership || !domains) {
return;
}

Expand All @@ -157,7 +157,7 @@ export const VerifiedDomainPage = withCardStateProvider(() => {
deletePending: deletePending.checked,
});

await (domains as any).unstable__mutate();
await domains.revalidate();

await navigate('../../');
} catch (e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useInView } from '../../hooks';
import type { PropsOfComponent } from '../../styledSystem';
import { common } from '../../styledSystem';
import { handleError } from '../../utils';
import { organizationListParams, removeItemFromPaginatedCache, updateCacheInPlace } from './utils';
import { organizationListParams, populateCacheRemoveItem, populateCacheUpdateItem } from './utils';

const useFetchInvitations = () => {
const { userInvitations, userSuggestions } = useCoreOrganizationList(organizationListParams);
Expand Down Expand Up @@ -44,7 +44,7 @@ const AcceptRejectSuggestionButtons = (props: OrganizationSuggestionResource) =>
const handleAccept = () => {
return card
.runAsync(props.accept)
.then(updateCacheInPlace(userSuggestions))
.then(updatedItem => userSuggestions?.setData?.(pages => populateCacheUpdateItem(updatedItem, pages)))
.catch(err => handleError(err, [], card.setError));
};

Expand Down Expand Up @@ -81,7 +81,7 @@ const AcceptRejectInvitationButtons = (props: UserOrganizationInvitationResource
const handleAccept = () => {
return card
.runAsync(props.accept)
.then(removeItemFromPaginatedCache(userInvitations))
.then(updatedItem => userInvitations?.setData?.(pages => populateCacheRemoveItem(updatedItem, pages)))
.catch(err => handleError(err, [], card.setError));
};

Expand Down
52 changes: 26 additions & 26 deletions packages/clerk-js/src/ui/components/OrganizationSwitcher/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,20 @@ export const organizationListParams = {

export const populateCacheUpdateItem = <T extends { id: string }>(
updatedItem: T,
itemsInfinitePages: ClerkPaginatedResponse<T>[],
itemsInfinitePages: (ClerkPaginatedResponse<T> | undefined)[] | undefined,
) => {
if (typeof itemsInfinitePages === 'undefined') {
return [{ data: [updatedItem], total_count: 1 }];
}

/**
* We should "preserve" an undefined page if one is found. For example if swr triggers 2 requests, page 1 & page2, and the request for page2 resolves first, at that point in memory itemsInfinitePages would look like this [undefined, {....}]
* if SWR says that has fetched 2 pages but the first result of is undefined, we should not return back an array with 1 item as this will end up having cacheKeys that point nowhere.
*/
return itemsInfinitePages.map(item => {
if (typeof item === 'undefined') {
return item;
}
const newData = item.data.map(obj => {
if (obj.id === updatedItem.id) {
return {
Expand All @@ -33,38 +44,27 @@ export const populateCacheUpdateItem = <T extends { id: string }>(
});
};

export const updateCacheInPlace =
(userSuggestions: any) =>
<T extends { id: string }>(result: T): any => {
userSuggestions?.unstable__mutate?.(result, {
populateCache: populateCacheUpdateItem,
// Since `accept` gives back the updated information,
// we don't need to revalidate here.
revalidate: false,
});
};

export const populateCacheRemoveItem = <T extends { id: string }>(
updatedItem: T,
itemsInfinitePages: ClerkPaginatedResponse<T>[],
itemsInfinitePages: (ClerkPaginatedResponse<T> | undefined)[] | undefined,
) => {
const prevTotalCount = itemsInfinitePages[itemsInfinitePages.length - 1].total_count;
const prevTotalCount = itemsInfinitePages?.[itemsInfinitePages.length - 1]?.total_count;

return itemsInfinitePages.map(item => {
if (!prevTotalCount) {
return undefined;
}

/**
* We should "preserve" an undefined page if one is found. For example if swr triggers 2 requests, page 1 & page2, and the request for page2 resolves first, at that point in memory itemsInfinitePages would look like this [undefined, {....}]
* if SWR says that has fetched 2 pages but the first result of is undefined, we should not return back an array with 1 item as this will end up having cacheKeys that point nowhere.
*/
return itemsInfinitePages?.map(item => {
if (typeof item === 'undefined') {
return item;
}
const newData = item.data.filter(obj => {
return obj.id !== updatedItem.id;
});
return { ...item, data: newData, total_count: prevTotalCount - 1 };
});
};

export const removeItemFromPaginatedCache =
(userInvitations: any) =>
<T extends { id: string }>(result: T): any => {
userInvitations?.unstable__mutate?.(result, {
populateCache: populateCacheRemoveItem,
// Since `accept` gives back the updated information,
// we don't need to revalidate here.
revalidate: false,
});
};
28 changes: 21 additions & 7 deletions packages/shared/src/react/hooks/useOrganization.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ type UseOrganizationParams = {
});
};

type UseOrganizationReturn =
type UseOrganization = <T extends UseOrganizationParams>(
params?: T,
) =>
| {
isLoaded: false;
organization: undefined;
Expand Down Expand Up @@ -103,14 +105,24 @@ type UseOrganizationReturn =
*/
membershipList: OrganizationMembershipResource[] | null | undefined;
membership: OrganizationMembershipResource | null | undefined;
domains: PaginatedResources<OrganizationDomainResource> | null;
membershipRequests: PaginatedResources<OrganizationMembershipRequestResource> | null;
memberships: PaginatedResources<OrganizationMembershipResource> | null;
invitations: PaginatedResources<OrganizationInvitationResource> | null;
domains: PaginatedResources<
OrganizationDomainResource,
T['membershipRequests'] extends { infinite: true } ? true : false
> | null;
membershipRequests: PaginatedResources<
OrganizationMembershipRequestResource,
T['membershipRequests'] extends { infinite: true } ? true : false
> | null;
memberships: PaginatedResources<
OrganizationMembershipResource,
T['memberships'] extends { infinite: true } ? true : false
> | null;
invitations: PaginatedResources<
OrganizationInvitationResource,
T['invitations'] extends { infinite: true } ? true : false
> | null;
};

type UseOrganization = (params?: UseOrganizationParams) => UseOrganizationReturn;

const undefinedPaginatedResource = {
data: undefined,
count: undefined,
Expand All @@ -124,6 +136,8 @@ const undefinedPaginatedResource = {
fetchPrevious: undefined,
hasNextPage: false,
hasPreviousPage: false,
revalidate: undefined,
setData: undefined,
} as const;

export const useOrganization: UseOrganization = params => {
Expand Down
53 changes: 31 additions & 22 deletions packages/shared/src/react/hooks/useOrganizationList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,26 @@ type UseOrganizationListParams = {
};

type OrganizationList = ReturnType<typeof createOrganizationList>;
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,
revalidate: undefined,
setData: undefined,
} as const;

type UseOrganizationListReturn =
type UseOrganizationList = <T extends UseOrganizationListParams>(
params?: T,
) =>
| {
isLoaded: false;
/**
Expand All @@ -60,29 +78,20 @@ type UseOrganizationListReturn =
organizationList: OrganizationList;
createOrganization: (params: CreateOrganizationParams) => Promise<OrganizationResource>;
setActive: SetActive;
userMemberships: PaginatedResources<OrganizationMembershipResource>;
userInvitations: PaginatedResources<UserOrganizationInvitationResource>;
userSuggestions: PaginatedResources<OrganizationSuggestionResource>;
userMemberships: PaginatedResources<
OrganizationMembershipResource,
T['userMemberships'] extends { infinite: true } ? true : false
>;
userInvitations: PaginatedResources<
UserOrganizationInvitationResource,
T['userInvitations'] extends { infinite: true } ? true : false
>;
userSuggestions: PaginatedResources<
OrganizationSuggestionResource,
T['userSuggestions'] extends { infinite: true } ? true : false
>;
};

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,
unstable__mutate: undefined,
} as const;

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

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

Expand Down
Loading

0 comments on commit cf7ead7

Please sign in to comment.