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).
magicznyleszek authored Jan 24, 2025

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent ea727fb commit b237f87
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
@@ -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 {
@@ -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) {
@@ -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)
2 changes: 2 additions & 0 deletions jsapp/js/api.endpoints.ts
Original file line number Diff line number Diff line change
@@ -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/',
1 change: 1 addition & 0 deletions jsapp/js/query/queryKeys.ts
Original file line number Diff line number Diff line change
@@ -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.