Skip to content

Commit c8aaa26

Browse files
committed
refactor(invites): use generated react-query
1 parent e6aee5a commit c8aaa26

File tree

13 files changed

+243
-271
lines changed

13 files changed

+243
-271
lines changed

jsapp/js/account/organization/InviteModal.tsx

Lines changed: 53 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,47 @@
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 { InviteeRoleEnum } from '#/api/models/inviteeRoleEnum'
8+
import {
9+
getOrganizationsInvitesListQueryKey,
10+
getOrganizationsInvitesRetrieveQueryKey,
11+
useOrganizationsInvitesCreate,
12+
} from '#/api/react-query/organization-invites'
813
import ButtonNew from '#/components/common/ButtonNew'
914
import Select from '#/components/common/Select'
15+
import type { FailResponse } from '#/dataInterface'
1016
import envStore from '#/envStore'
17+
import { queryClient } from '#/query/queryClient'
18+
import { QueryKeys } from '#/query/queryKeys'
1119
import userExistence from '#/users/userExistence.store'
1220
import { checkEmailPattern, notify } from '#/utils'
13-
import { useSendMemberInvite } from './membersInviteQuery'
14-
import { OrganizationUserRole } from './organizationQuery'
21+
import { inviteGuidFromUrl } from './common'
22+
import { useOrganizationQuery } from './organizationQuery'
1523

1624
export default function InviteModal(props: ModalProps) {
17-
const inviteQuery = useSendMemberInvite()
25+
const inviteQuery = useOrganizationsInvitesCreate({
26+
mutation: {
27+
onSuccess: (data, variables) => {
28+
if (data.status !== 201) return // typeguard, `onSuccess` will always be 201.
29+
queryClient.invalidateQueries({ queryKey: getOrganizationsInvitesListQueryKey(variables.organizationId) })
30+
for (const invite of data.data) {
31+
queryClient.invalidateQueries({
32+
queryKey: getOrganizationsInvitesRetrieveQueryKey(variables.organizationId, inviteGuidFromUrl(invite.url)),
33+
})
34+
}
35+
queryClient.invalidateQueries({ queryKey: [QueryKeys.organizationMembers] })
36+
queryClient.invalidateQueries({ queryKey: [QueryKeys.organizationMemberDetail] })
37+
},
38+
},
39+
})
40+
const orgQuery = useOrganizationQuery()
41+
const organizationId = orgQuery.data?.id
1842
const mmoLabel = getSimpleMMOLabel(envStore.data, subscriptionStore.activeSubscriptions[0])
1943

20-
const [role, setRole] = useState<string | null>(null)
44+
const [role, setRole] = useState<InviteeRoleEnum | null>(null)
2145

2246
async function handleUsernameOrEmailCheck(value: string) {
2347
if (value === '' || checkEmailPattern(value)) {
@@ -38,25 +62,28 @@ export default function InviteModal(props: ModalProps) {
3862
validateOnBlur: true,
3963
})
4064

41-
const handleSendInvite = () => {
42-
if (role) {
43-
inviteQuery
44-
.mutateAsync({
65+
const handleSendInvite = async () => {
66+
if (!organizationId) return
67+
if (!role) return
68+
try {
69+
await inviteQuery.mutateAsync({
70+
organizationId,
71+
data: {
4572
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-
})
73+
role: role,
74+
},
75+
})
76+
userOrEmail.reset()
77+
setRole(null)
78+
props.onClose()
79+
} catch (error) {
80+
const responseText = (error as FailResponse).responseText
81+
if (responseText && JSON.parse(responseText)?.invitees) {
82+
notify(JSON.parse(responseText)?.invitees.join(), 'error')
83+
} else {
84+
console.error(error)
85+
notify(t('Failed to send invite'), 'error')
86+
}
6087
}
6188
}
6289

@@ -89,11 +116,11 @@ export default function InviteModal(props: ModalProps) {
89116
placeholder='Role'
90117
data={[
91118
{
92-
value: OrganizationUserRole.admin,
119+
value: InviteeRoleEnum.admin,
93120
label: t('Admin'),
94121
},
95122
{
96-
value: OrganizationUserRole.member,
123+
value: InviteeRoleEnum.member,
97124
label: t('Member'),
98125
},
99126
]}

jsapp/js/account/organization/InviteeActionsDropdown.tsx

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,20 @@ 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 { InviteStatusChoicesEnum } from '#/api/models/inviteStatusChoicesEnum'
7+
import {
8+
getOrganizationsInvitesListQueryKey,
9+
getOrganizationsInvitesRetrieveQueryKey,
10+
useOrganizationsInvitesDestroy,
11+
useOrganizationsInvitesPartialUpdate,
12+
} from '#/api/react-query/organization-invites'
513
import ButtonNew from '#/components/common/ButtonNew'
14+
import { queryClient } from '#/query/queryClient'
15+
import { QueryKeys } from '#/query/queryKeys'
616
import { notify } from '#/utils'
7-
import type { MemberInvite } from './membersInviteQuery'
8-
import { MemberInviteStatus, usePatchMemberInvite, useRemoveMemberInvite } from './membersInviteQuery'
17+
import { inviteGuidFromUrl } from './common'
18+
import { useOrganizationQuery } from './organizationQuery'
919

1020
/**
1121
* A dropdown with all actions that can be taken towards an organization invitee.
@@ -15,17 +25,50 @@ export default function InviteeActionsDropdown({
1525
invite,
1626
}: {
1727
target: ReactNode
18-
invite: MemberInvite
28+
invite: InviteResponse
1929
}) {
30+
const orgQuery = useOrganizationQuery()
31+
const organizationId = orgQuery.data?.id
32+
2033
const [opened, { open, close }] = useDisclosure()
2134

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

2563
const resendInvitation = async () => {
64+
if (!organizationId) return
2665
try {
27-
await patchInviteMutation.mutateAsync({
28-
status: MemberInviteStatus.resent,
66+
await orgInvitesPatchMutation.mutateAsync({
67+
organizationId,
68+
guid: inviteGuidFromUrl(invite.url),
69+
data: {
70+
status: InviteStatusChoicesEnum.resent,
71+
},
2972
})
3073
notify(t('The invitation was resent'), 'success')
3174
} catch (e: any) {
@@ -49,8 +92,9 @@ export default function InviteeActionsDropdown({
4992
}
5093

5194
const removeInvitation = async () => {
95+
if (!organizationId) return
5296
try {
53-
await removeInviteMutation.mutateAsync(invite.url)
97+
await orgInvitesDestroyMutation.mutateAsync({ organizationId, guid: inviteGuidFromUrl(invite.url) })
5498
notify(t('Invitation removed'), 'success')
5599
} catch (e) {
56100
notify(t('An error occurred while removing the invitation'), 'error')
@@ -62,7 +106,7 @@ export default function InviteeActionsDropdown({
62106
return (
63107
<>
64108
<Modal opened={opened} onClose={close} title={t('Remove invitation?')}>
65-
<LoadingOverlay visible={removeInviteMutation.isPending} />
109+
<LoadingOverlay visible={orgInvitesDestroyMutation.isPending} />
66110
<Stack>
67111
<Text>{t("Are you sure you want to remove this user's invitation to join the team?")}</Text>
68112
<Group justify='flex-end'>
@@ -76,7 +120,7 @@ export default function InviteeActionsDropdown({
76120
</Stack>
77121
</Modal>
78122

79-
<LoadingOverlay visible={patchInviteMutation.isPending} />
123+
<LoadingOverlay visible={orgInvitesPatchMutation.isPending} />
80124
<Menu offset={0} position='bottom-end'>
81125
<Menu.Target>{target}</Menu.Target>
82126

jsapp/js/account/organization/MemberRoleSelector.tsx

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
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 { inviteGuidFromUrl } from './common'
11+
import { type OrganizationMemberListItem, usePatchOrganizationMember } from './membersQuery'
12+
import { OrganizationUserRole, useOrganizationQuery } from './organizationQuery'
613

714
interface MemberRoleSelectorProps {
815
username: string
@@ -15,23 +22,65 @@ interface MemberRoleSelectorProps {
1522
}
1623

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

2165
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-
}
66+
if (!organizationId) return
67+
if (!newRole) return
68+
const role = newRole as OrganizationUserRole
69+
70+
if (inviteUrl) {
71+
orgInvitesPatchMutation.mutateAsync({
72+
guid: inviteGuidFromUrl(inviteUrl),
73+
organizationId,
74+
data: { role },
75+
})
76+
} else {
77+
patchMember.mutateAsync({ role })
2978
}
3079
}
3180

3281
return (
3382
<>
34-
<LoadingOverlay visible={patchMember.isPending || patchInvite.isPending} />
83+
<LoadingOverlay visible={patchMember.isPending || orgInvitesPatchMutation.isPending} />
3584
<Select
3685
size='sm'
3786
data={[

jsapp/js/account/organization/MembersRoute.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import UniversalTable, { DEFAULT_PAGE_SIZE, type UniversalTableColumn } from '#/
77
import InviteModal from '#/account/organization/InviteModal'
88
import { getSimpleMMOLabel } from '#/account/organization/organization.utils'
99
import subscriptionStore from '#/account/subscriptionStore'
10+
import { InviteStatusChoicesEnum } from '#/api/models/inviteStatusChoicesEnum'
1011
import ActionIcon from '#/components/common/ActionIcon'
1112
import ButtonNew from '#/components/common/ButtonNew'
1213
import Avatar from '#/components/common/avatar'
@@ -52,7 +53,10 @@ export default function MembersRoute() {
5253
* Returns both an invite and member, but one of these will be null depending on status
5354
*/
5455
function getMemberOrInviteDetails(obj: OrganizationMemberListItem) {
55-
const invite = obj.invite?.status === 'pending' || obj.invite?.status === 'resent' ? obj.invite : null
56+
const invite =
57+
obj.invite?.status === InviteStatusChoicesEnum.pending || obj.invite?.status === InviteStatusChoicesEnum.resent
58+
? obj.invite
59+
: null
5660
const member = invite ? null : ({ ...obj } as OrganizationMember)
5761
return { invite, member }
5862
}
@@ -100,7 +104,7 @@ export default function MembersRoute() {
100104
size: 140,
101105
cellFormatter: (obj: OrganizationMemberListItem) => {
102106
const { invite, member } = getMemberOrInviteDetails(obj)
103-
return invite ? formatDate(invite.date_created) : formatDate(member!.date_joined)
107+
return invite ? formatDate(invite.created) : formatDate(member!.date_joined)
104108
},
105109
},
106110
{
@@ -130,7 +134,7 @@ export default function MembersRoute() {
130134
return (
131135
<MemberRoleSelector
132136
username={invite.invitee}
133-
role={invite.invitee_role}
137+
role={invite.invitee_role as any}
134138
currentUserRole={orgQuery.data.request_user_role}
135139
inviteUrl={invite.url}
136140
/>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/**
2+
* Note: invites APIs return objects with a URL property with an ID within but no ID property.
3+
*/
4+
export const inviteGuidFromUrl = (url: string) => url.slice(0, -1).split('/').pop()!

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)