Skip to content

Commit bfbd9e4

Browse files
committed
refactor(invites): use generated react-query
1 parent 3a900e6 commit bfbd9e4

File tree

9 files changed

+219
-260
lines changed

9 files changed

+219
-260
lines changed

jsapp/js/account/organization/InviteModal.tsx

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
1-
import { useState } from 'react'
2-
31
import type { ModalProps } from '@mantine/core'
42
import { Group, Loader, Modal, Stack, Text, TextInput } from '@mantine/core'
53
import { useField } from '@mantine/form'
4+
import { useState } from 'react'
65
import { getSimpleMMOLabel } from '#/account/organization/organization.utils'
76
import subscriptionStore from '#/account/subscriptionStore'
7+
import {
8+
getOrganizationsInvitesListQueryKey,
9+
getOrganizationsInvitesRetrieveQueryKey,
10+
useOrganizationsInvitesCreate,
11+
} from '#/api/react-query/organization-invites'
812
import ButtonNew from '#/components/common/ButtonNew'
913
import Select from '#/components/common/Select'
14+
import type { FailResponse } from '#/dataInterface'
1015
import envStore from '#/envStore'
16+
import { queryClient } from '#/query/queryClient'
17+
import { QueryKeys } from '#/query/queryKeys'
1118
import userExistence from '#/users/userExistence.store'
1219
import { checkEmailPattern, notify } from '#/utils'
13-
import { useSendMemberInvite } from './membersInviteQuery'
14-
import { OrganizationUserRole } from './organizationQuery'
20+
import { OrganizationUserRole, useOrganizationQuery } from './organizationQuery'
1521

1622
export default function InviteModal(props: ModalProps) {
17-
const inviteQuery = useSendMemberInvite()
23+
const inviteQuery = useOrganizationsInvitesCreate()
24+
const orgQuery = useOrganizationQuery()
25+
const organizationId = orgQuery.data?.id
1826
const mmoLabel = getSimpleMMOLabel(envStore.data, subscriptionStore.activeSubscriptions[0])
1927

2028
const [role, setRole] = useState<string | null>(null)
@@ -38,25 +46,48 @@ export default function InviteModal(props: ModalProps) {
3846
validateOnBlur: true,
3947
})
4048

41-
const handleSendInvite = () => {
42-
if (role) {
43-
inviteQuery
44-
.mutateAsync({
45-
invitees: [userOrEmail.getValue()],
46-
role: role as OrganizationUserRole,
47-
})
48-
.then(() => {
49-
userOrEmail.reset()
50-
setRole(null)
51-
props.onClose()
52-
})
53-
.catch((error) => {
54-
if (error.responseText && JSON.parse(error.responseText)?.invitees) {
55-
notify(JSON.parse(error.responseText)?.invitees.join(), 'error')
56-
} else {
57-
notify(t('Failed to send invite'), 'error')
58-
}
59-
})
49+
const handleSendInvite = async () => {
50+
if (!organizationId) return
51+
if (!role) return
52+
try {
53+
await inviteQuery.mutateAsync(
54+
{
55+
organizationId,
56+
data: {
57+
invitees: [userOrEmail.getValue()],
58+
role: role as OrganizationUserRole,
59+
},
60+
},
61+
{
62+
onSuccess: (_data, variables) => {
63+
// @ts-expect-error TODO: fix schema at kpi#6122
64+
if (_data.status !== 201) return // typeguard, `onSuccess` will always be 200.
65+
queryClient.invalidateQueries({ queryKey: getOrganizationsInvitesListQueryKey(variables.organizationId) })
66+
// @ts-expect-error TODO: fix schema at kpi#6122
67+
for (const invite of _data.data) {
68+
queryClient.invalidateQueries({
69+
queryKey: getOrganizationsInvitesRetrieveQueryKey(
70+
variables.organizationId,
71+
invite.url.slice(0, -1).split('/').pop()!,
72+
),
73+
})
74+
}
75+
queryClient.invalidateQueries({ queryKey: [QueryKeys.organizationMembers] })
76+
queryClient.invalidateQueries({ queryKey: [QueryKeys.organizationMemberDetail] })
77+
},
78+
},
79+
)
80+
userOrEmail.reset()
81+
setRole(null)
82+
props.onClose()
83+
} catch (error) {
84+
const responseText = (error as FailResponse).responseText
85+
if (responseText && JSON.parse(responseText)?.invitees) {
86+
notify(JSON.parse(responseText)?.invitees.join(), 'error')
87+
} else {
88+
console.error(error)
89+
notify(t('Failed to send invite'), 'error')
90+
}
6091
}
6192
}
6293

jsapp/js/account/organization/InviteeActionsDropdown.tsx

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,18 @@ import type { ReactNode } from 'react'
22

33
import { Group, LoadingOverlay, Menu, Modal, Stack, Text } from '@mantine/core'
44
import { useDisclosure } from '@mantine/hooks'
5+
import type { InviteResponse } from '#/api/models/inviteResponse'
6+
import {
7+
getOrganizationsInvitesListQueryKey,
8+
getOrganizationsInvitesRetrieveQueryKey,
9+
useOrganizationsInvitesDestroy,
10+
useOrganizationsInvitesPartialUpdate,
11+
} from '#/api/react-query/organization-invites'
512
import ButtonNew from '#/components/common/ButtonNew'
13+
import { queryClient } from '#/query/queryClient'
14+
import { QueryKeys } from '#/query/queryKeys'
615
import { notify } from '#/utils'
7-
import type { MemberInvite } from './membersInviteQuery'
8-
import { MemberInviteStatus, usePatchMemberInvite, useRemoveMemberInvite } from './membersInviteQuery'
16+
import { useOrganizationQuery } from './organizationQuery'
917

1018
/**
1119
* A dropdown with all actions that can be taken towards an organization invitee.
@@ -15,17 +23,49 @@ export default function InviteeActionsDropdown({
1523
invite,
1624
}: {
1725
target: ReactNode
18-
invite: MemberInvite
26+
invite: InviteResponse
1927
}) {
28+
const orgQuery = useOrganizationQuery()
29+
const organizationId = orgQuery.data?.id!
30+
2031
const [opened, { open, close }] = useDisclosure()
2132

22-
const patchInviteMutation = usePatchMemberInvite(invite.url)
23-
const removeInviteMutation = useRemoveMemberInvite()
33+
const orgInvitesPatchMutation = useOrganizationsInvitesPartialUpdate({
34+
mutation: {
35+
onSuccess: (_data, variables) => {
36+
queryClient.invalidateQueries({ queryKey: getOrganizationsInvitesListQueryKey(variables.organizationId) })
37+
queryClient.invalidateQueries({
38+
queryKey: getOrganizationsInvitesRetrieveQueryKey(variables.organizationId, variables.guid),
39+
})
40+
queryClient.invalidateQueries({ queryKey: [QueryKeys.organizationMembers] })
41+
queryClient.invalidateQueries({ queryKey: [QueryKeys.organizationMemberDetail] })
42+
},
43+
},
44+
request: {
45+
errorMessageDisplay: t('There was an error updating this invitation.'),
46+
},
47+
})
48+
const orgInvitesDestroyMutation = useOrganizationsInvitesDestroy({
49+
mutation: {
50+
onSuccess: (_data, variables) => {
51+
queryClient.invalidateQueries({ queryKey: getOrganizationsInvitesListQueryKey(variables.organizationId) })
52+
queryClient.invalidateQueries({
53+
queryKey: getOrganizationsInvitesRetrieveQueryKey(variables.organizationId, variables.guid),
54+
})
55+
queryClient.invalidateQueries({ queryKey: [QueryKeys.organizationMembers] })
56+
queryClient.invalidateQueries({ queryKey: [QueryKeys.organizationMemberDetail] })
57+
},
58+
},
59+
})
2460

2561
const resendInvitation = async () => {
2662
try {
27-
await patchInviteMutation.mutateAsync({
28-
status: MemberInviteStatus.resent,
63+
await orgInvitesPatchMutation.mutateAsync({
64+
organizationId,
65+
guid: invite.url.slice(0, -1).split('/').pop()!,
66+
data: {
67+
status: 'resent', // TODO: should be `InviteStatusChoicesEnum.resent`
68+
},
2969
})
3070
notify(t('The invitation was resent'), 'success')
3171
} catch (e: any) {
@@ -50,7 +90,7 @@ export default function InviteeActionsDropdown({
5090

5191
const removeInvitation = async () => {
5292
try {
53-
await removeInviteMutation.mutateAsync(invite.url)
93+
await orgInvitesDestroyMutation.mutateAsync({ organizationId, guid: invite.url.slice(0, -1).split('/').pop()! })
5494
notify(t('Invitation removed'), 'success')
5595
} catch (e) {
5696
notify(t('An error occurred while removing the invitation'), 'error')
@@ -62,7 +102,7 @@ export default function InviteeActionsDropdown({
62102
return (
63103
<>
64104
<Modal opened={opened} onClose={close} title={t('Remove invitation?')}>
65-
<LoadingOverlay visible={removeInviteMutation.isPending} />
105+
<LoadingOverlay visible={orgInvitesDestroyMutation.isPending} />
66106
<Stack>
67107
<Text>{t("Are you sure you want to remove this user's invitation to join the team?")}</Text>
68108
<Group justify='flex-end'>
@@ -76,7 +116,7 @@ export default function InviteeActionsDropdown({
76116
</Stack>
77117
</Modal>
78118

79-
<LoadingOverlay visible={patchInviteMutation.isPending} />
119+
<LoadingOverlay visible={orgInvitesPatchMutation.isPending} />
80120
<Menu offset={0} position='bottom-end'>
81121
<Menu.Target>{target}</Menu.Target>
82122

jsapp/js/account/organization/MemberRoleSelector.tsx

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import { LoadingOverlay } from '@mantine/core'
2+
import {
3+
getOrganizationsInvitesListQueryKey,
4+
getOrganizationsInvitesRetrieveQueryKey,
5+
useOrganizationsInvitesPartialUpdate,
6+
} from '#/api/react-query/organization-invites'
27
import Select from '#/components/common/Select'
3-
import { usePatchMemberInvite } from './membersInviteQuery'
4-
import { usePatchOrganizationMember } from './membersQuery'
5-
import { OrganizationUserRole } from './organizationQuery'
8+
import { queryClient } from '#/query/queryClient'
9+
import { QueryKeys } from '#/query/queryKeys'
10+
import { type OrganizationMemberListItem, usePatchOrganizationMember } from './membersQuery'
11+
import { OrganizationUserRole, useOrganizationQuery } from './organizationQuery'
612

713
interface MemberRoleSelectorProps {
814
username: string
@@ -15,23 +21,65 @@ interface MemberRoleSelectorProps {
1521
}
1622

1723
export default function MemberRoleSelector({ username, role, inviteUrl }: MemberRoleSelectorProps) {
24+
const orgQuery = useOrganizationQuery()
25+
const organizationId = orgQuery.data?.id
26+
1827
const patchMember = usePatchOrganizationMember(username)
19-
const patchInvite = usePatchMemberInvite(inviteUrl)
28+
29+
const orgInvitesPatchMutation = useOrganizationsInvitesPartialUpdate({
30+
mutation: {
31+
onSuccess: (_data, variables) => {
32+
queryClient.invalidateQueries({ queryKey: getOrganizationsInvitesListQueryKey(variables.organizationId) })
33+
queryClient.invalidateQueries({
34+
queryKey: getOrganizationsInvitesRetrieveQueryKey(variables.organizationId, variables.guid),
35+
})
36+
queryClient.invalidateQueries({ queryKey: [QueryKeys.organizationMembers], })
37+
queryClient.invalidateQueries({ queryKey: [QueryKeys.organizationMemberDetail], })
38+
},
39+
onMutate: async ({data}) => {
40+
if (!('role' in data)) return
41+
42+
// If we are updating the invitee's role, we want to optimistically update their role in queries for
43+
// the members table list. So we look for their unique invite url and update the relevant query accordingly
44+
const qData = queryClient.getQueriesData({ queryKey: [QueryKeys.organizationMembers] })
45+
const query = qData.find((q) =>
46+
(q[1] as any)?.results?.find((m: OrganizationMemberListItem) => m.invite?.url === inviteUrl),
47+
)
48+
49+
if (!query) return
50+
51+
const queryKey = query[0]
52+
const queryData = query[1]
53+
const item = (queryData as any).results.find((m: OrganizationMemberListItem) => m.invite?.url === inviteUrl)
54+
55+
item.invite.invitee_role = data.role
56+
queryClient.setQueryData(queryKey, queryData)
57+
},
58+
},
59+
request: {
60+
errorMessageDisplay: t('There was an error updating this invitation.')
61+
}
62+
})
2063

2164
const handleRoleChange = (newRole: string | null) => {
22-
if (newRole) {
23-
const role = newRole as OrganizationUserRole
24-
if (inviteUrl) {
25-
patchInvite.mutateAsync({ role })
26-
} else {
27-
patchMember.mutateAsync({ role })
28-
}
65+
if (!organizationId) return
66+
if (!newRole) return
67+
const role = newRole as OrganizationUserRole
68+
69+
if (inviteUrl) {
70+
orgInvitesPatchMutation.mutateAsync({
71+
guid: inviteUrl.slice(0, -1).split('/').pop()!,
72+
organizationId,
73+
data: { role },
74+
})
75+
} else {
76+
patchMember.mutateAsync({ role })
2977
}
3078
}
3179

3280
return (
3381
<>
34-
<LoadingOverlay visible={patchMember.isPending || patchInvite.isPending} />
82+
<LoadingOverlay visible={patchMember.isPending || orgInvitesPatchMutation.isPending} />
3583
<Select
3684
size='sm'
3785
data={[

jsapp/js/account/organization/MembersRoute.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export default function MembersRoute() {
3232
* Returns both an invite and member, but one of these will be null depending on status
3333
*/
3434
function getMemberOrInviteDetails(obj: OrganizationMemberListItem) {
35+
// @ts-expect-error TODO: fix schema
3536
const invite = obj.invite?.status === 'pending' || obj.invite?.status === 'resent' ? obj.invite : null
3637
const member = invite ? null : ({ ...obj } as OrganizationMember)
3738
return { invite, member }
@@ -80,7 +81,7 @@ export default function MembersRoute() {
8081
size: 140,
8182
cellFormatter: (obj: OrganizationMemberListItem) => {
8283
const { invite, member } = getMemberOrInviteDetails(obj)
83-
return invite ? formatDate(invite.date_created) : formatDate(member!.date_joined)
84+
return invite ? formatDate(invite.created) : formatDate(member!.date_joined)
8485
},
8586
},
8687
{
@@ -110,7 +111,7 @@ export default function MembersRoute() {
110111
return (
111112
<MemberRoleSelector
112113
username={invite.invitee}
113-
role={invite.invitee_role}
114+
role={invite.invitee_role as any}
114115
currentUserRole={orgQuery.data.request_user_role}
115116
inviteUrl={invite.url}
116117
/>

jsapp/js/account/organization/invites/OrgInviteAcceptedBanner.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import React, { useEffect, useState } from 'react'
2-
3-
import { MemberInviteStatus } from '#/account/organization/membersInviteQuery'
42
import { useOrganizationMemberDetailQuery } from '#/account/organization/membersQuery'
53
import type { Organization } from '#/account/organization/organizationQuery'
4+
import { InviteStatusChoicesEnum } from '#/api/models/inviteStatusChoicesEnum'
65
import Alert from '#/components/common/alert'
76
import { useSafeUsernameStorageKey } from '#/hooks/useSafeUsernameStorageKey'
87

@@ -52,7 +51,7 @@ export default function OrgInviteAcceptedBanner(props: OrgInviteAcceptedBannerPr
5251
// Only show banner to users who are members of MMO organization
5352
!props.organization.is_mmo ||
5453
// Only show banner to users who have accepted the invite
55-
organizationMemberDetailQuery.data?.invite?.status !== MemberInviteStatus.accepted ||
54+
organizationMemberDetailQuery.data?.invite?.status !== InviteStatusChoicesEnum.accepted ||
5655
// Wait for local storage information
5756
isBannerDismissed === undefined ||
5857
// Respect users who dismissed the banner

0 commit comments

Comments
 (0)