Skip to content

Commit

Permalink
feat(invites): query file for handling API TASK-1092 TASK-991 (#5409)
Browse files Browse the repository at this point in the history
### 📣 Summary
A query file that allows using new invitation system API. The
functionalities are:
- send invite (for admins to invite users to their org)
- remove invite (for admins to cancel invite)
- get single invite (for users to get invite info after visiting link
from email with `?params`)
- patch single invite (for admins to resend invite, for users to accept
or decline invite)

All the actions ensure `membersQuery` is up to date (it's being used as
the source of data for the list of invites).
  • Loading branch information
magicznyleszek authored Jan 24, 2025
1 parent ea727fb commit b237f87
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 10 deletions.
136 changes: 136 additions & 0 deletions jsapp/js/account/organization/membersInviteQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import {
useQuery,
useQueryClient,
useMutation,
} from '@tanstack/react-query';
import {fetchPost, fetchGet, fetchPatchUrl, fetchDeleteUrl} from 'js/api';
import {type OrganizationUserRole, useOrganizationQuery} from './organizationQuery';
import {QueryKeys} from 'js/query/queryKeys';
import {endpoints} from 'jsapp/js/api.endpoints';
import type {FailResponse} from 'jsapp/js/dataInterface';
import {type OrganizationMember} from './membersQuery';
import {type Json} from 'jsapp/js/components/common/common.interfaces';

/*
* NOTE: `invites` - `membersQuery` holds a list of members, each containing
* an optional `invite` property (i.e. invited users that are not members yet
* will also appear on that list). That's why we have mutation hooks here for
* managing the invites. And each mutation will invalidate `membersQuery` to
* make it refetch.
*/

/*
* NOTE: `orgId` - we're assuming it is not `undefined` in code below,
* because the parent query (`useOrganizationMembersQuery`) wouldn't be enabled
* without it. Plus all the organization-related UI (that would use this hook)
* is accessible only to logged in users.
*/

/**
* The source of truth of statuses are at `OrganizationInviteStatusChoices` in
* `kobo/apps/organizations/models.py`. This enum should be kept in sync.
*/
enum MemberInviteStatus {
accepted = 'accepted',
cancelled = 'cancelled',
complete = 'complete',
declined = 'declined',
expired = 'expired',
failed = 'failed',
in_progress = 'in_progress',
pending = 'pending',
resent = 'resent',
}

export interface MemberInvite {
/** This is `endpoints.ORG_INVITE_URL`. */
url: string;
/** Url of a user that have sent the invite. */
invited_by: string;
status: MemberInviteStatus;
/** Username of user being invited. */
invitee: string;
/** Target role of user being invited. */
invitee_role: OrganizationUserRole;
/** Date format `yyyy-mm-dd HH:MM:SS`. */
date_created: string;
/** Date format: `yyyy-mm-dd HH:MM:SS`. */
date_modified: string;
}

interface SendMemberInviteParams {
/** List of usernames. */
invitees: string[];
/** Target role for the invitied users. */
role: OrganizationUserRole;
}

/**
* Mutation hook that allows sending invite for given user to join organization
* (of logged in user). It ensures that `membersQuery` will refetch data (by
* invalidation).
*/
export function useSendMemberInvite() {
const queryClient = useQueryClient();
const orgQuery = useOrganizationQuery();
const orgId = orgQuery.data?.id;
return useMutation({
mutationFn: async (payload: SendMemberInviteParams & Json) => {
const apiPath = endpoints.ORG_MEMBER_INVITES_URL.replace(':organization_id', orgId!);
fetchPost<OrganizationMember>(apiPath, payload);
},
onSettled: () => {
queryClient.invalidateQueries({queryKey: [QueryKeys.organizationMembers]});
},
});
}

/**
* Mutation hook that allows removing existing invite. It ensures that
* `membersQuery` will refetch data (by invalidation).
*/
export function useRemoveMemberInvite() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (inviteUrl: string) => {
fetchDeleteUrl<OrganizationMember>(inviteUrl);
},
onSettled: () => {
queryClient.invalidateQueries({queryKey: [QueryKeys.organizationMembers]});
},
});
}

/**
* A hook that gives you a single organization member invite.
*/
export const useOrgMemberInviteQuery = (orgId: string, inviteId: string) => {
const apiPath = endpoints.ORG_MEMBER_INVITE_DETAIL_URL
.replace(':organization_id', orgId!)
.replace(':invite_id', inviteId);
return useQuery<MemberInvite, FailResponse>({
queryFn: () => fetchGet<MemberInvite>(apiPath),
queryKey: [QueryKeys.organizationMemberInviteDetail, apiPath],
});
};

/**
* Mutation hook that allows patching existing invite. Use it to change
* the status of the invite (e.g. decline invite). It ensures that both
* `membersQuery` and `useOrgMemberInviteQuery` will refetch data (by
* invalidation).
*/
export function usePatchMemberInvite(inviteUrl: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (newInviteData: Partial<MemberInvite>) => {
fetchPatchUrl<OrganizationMember>(inviteUrl, newInviteData);
},
onSettled: () => {
queryClient.invalidateQueries({queryKey: [
QueryKeys.organizationMemberInviteDetail,
QueryKeys.organizationMembers,
]});
},
});
}
14 changes: 4 additions & 10 deletions jsapp/js/account/organization/membersQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {endpoints} from 'js/api.endpoints';
import type {PaginatedResponse} from 'js/dataInterface';
import {QueryKeys} from 'js/query/queryKeys';
import type {PaginatedQueryHookParams} from 'jsapp/js/universalTable/paginatedQueryUniversalTable.component';
import type {MemberInvite} from './membersInviteQuery';
import type {Json} from 'jsapp/js/components/common/common.interfaces';
import {useSession} from 'jsapp/js/stores/useSession';

export interface OrganizationMember {
Expand All @@ -38,15 +40,7 @@ export interface OrganizationMember {
user__is_active: boolean;
/** yyyy-mm-dd HH:MM:SS */
date_joined: string;
invite?: {
/** '/api/v2/organizations/<organization_uid>/invites/<invite_uid>/' */
url: string;
/** yyyy-mm-dd HH:MM:SS */
date_created: string;
/** yyyy-mm-dd HH:MM:SS */
date_modified: string;
status: 'sent' | 'accepted' | 'expired' | 'declined';
};
invite?: MemberInvite;
}

function getMemberEndpoint(orgId: string, username: string) {
Expand All @@ -72,7 +66,7 @@ export function usePatchOrganizationMember(username: string) {
// query (`useOrganizationMembersQuery`) wouldn't be enabled without it.
// Plus all the organization-related UI (that would use this hook) is
// accessible only to logged in users.
fetchPatch<OrganizationMember>(getMemberEndpoint(orgId!, username), data),
fetchPatch<OrganizationMember>(getMemberEndpoint(orgId!, username), data as Json),
onSettled: () => {
// We invalidate query, so it will refetch (instead of refetching it
// directly, see: https://github.com/TanStack/query/discussions/2468)
Expand Down
2 changes: 2 additions & 0 deletions jsapp/js/api.endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export const endpoints = {
ASSET_HISTORY_EXPORT: '/api/v2/assets/:asset_uid/history/export/',
ASSET_URL: '/api/v2/assets/:uid/',
ORG_ASSETS_URL: '/api/v2/organizations/:organization_id/assets/',
ORG_MEMBER_INVITES_URL: '/api/v2/organizations/:organization_id/invites/',
ORG_MEMBER_INVITE_DETAIL_URL: '/api/v2/organizations/:organization_id/invites/:invite_id/',
ME_URL: '/me/',
PRODUCTS_URL: '/api/v2/stripe/products/',
SUBSCRIPTION_URL: '/api/v2/stripe/subscriptions/',
Expand Down
1 change: 1 addition & 0 deletions jsapp/js/query/queryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ export enum QueryKeys {
activityLogsFilter = 'activityLogsFilter',
organization = 'organization',
organizationMembers = 'organizationMembers',
organizationMemberInviteDetail = 'organizationMemberInviteDetail',
}

0 comments on commit b237f87

Please sign in to comment.