Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(invites): query file for handling API TASK-1092 TASK-991 #5409

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -3,6 +3,8 @@ export const endpoints = {
ASSET_HISTORY_ACTIONS: '/api/v2/assets/:asset_uid/history/actions',
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',
}
Loading