ApiAuthorization.Valid.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. import { zodResolver } from '@hookform/resolvers/zod'
  2. import { useEffect, useMemo, useState, type ReactNode } from 'react'
  3. import { useForm, type UseFormReturn } from 'react-hook-form'
  4. import { toast } from 'sonner'
  5. import { ApiAuthorizationApprovedScreen } from './ApiAuthorization.Approved'
  6. import { ApiAuthorizationErrorScreen } from './ApiAuthorization.Error'
  7. import { ApiAuthorizationMainView } from './ApiAuthorization.Form'
  8. import { ApiAuthorizationLoadingScreen } from './ApiAuthorization.Loading'
  9. import {
  10. approvalFormSchema,
  11. type ApprovalState,
  12. type IApprovalFormSchema,
  13. } from './ApiAuthorization.Schema'
  14. import { useApiAuthorizationApproveMutation } from '@/data/api-authorization/api-authorization-approve-mutation'
  15. import { useApiAuthorizationDeclineMutation } from '@/data/api-authorization/api-authorization-decline-mutation'
  16. import { useApiAuthorizationQuery } from '@/data/api-authorization/api-authorization-query'
  17. import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
  18. import { useStaticEffectEvent } from '@/hooks/useStaticEffectEvent'
  19. import type { Organization } from '@/types'
  20. function getMatchingOrganization(
  21. organization_slug: string | undefined,
  22. organizations: Array<Organization> | undefined
  23. ): Organization | null {
  24. if (!organization_slug || !organizations) return null
  25. return organizations.find(({ slug }) => slug === organization_slug) ?? null
  26. }
  27. interface PreselectOrganizationSlugParameters {
  28. form: UseFormReturn<IApprovalFormSchema>
  29. organization_slug: string | undefined
  30. organizations: Array<{ slug: string }>
  31. }
  32. function preselectOrganizationSlug({
  33. form,
  34. organization_slug,
  35. organizations,
  36. }: PreselectOrganizationSlugParameters) {
  37. if (organization_slug) {
  38. const preselected = organizations.find(({ slug }) => slug === organization_slug)
  39. if (preselected) form.setValue('selectedOrgSlug', preselected.slug)
  40. } else if (!form.getValues('selectedOrgSlug') && organizations.length === 1) {
  41. form.setValue('selectedOrgSlug', organizations[0].slug)
  42. }
  43. }
  44. function useOrganizationsState(organization_slug: string | undefined) {
  45. const {
  46. data: organizations,
  47. isPending: isLoadingOrganizations,
  48. isError: isErrorOrganizations,
  49. error: organizationsError,
  50. } = useOrganizationsQuery()
  51. const organizationsState = useMemo(
  52. function calculateOrganizationsState() {
  53. if (isLoadingOrganizations) {
  54. return { _tag: 'loading' as const }
  55. }
  56. if (isErrorOrganizations) {
  57. return { _tag: 'error' as const, error: organizationsError }
  58. }
  59. if (organizations.length === 0) {
  60. return { _tag: 'empty' as const }
  61. }
  62. if (organization_slug) {
  63. const matchingOrganization = getMatchingOrganization(organization_slug, organizations)
  64. if (!matchingOrganization) {
  65. return { _tag: 'not_member' as const }
  66. }
  67. }
  68. return { _tag: 'success' as const, organizations }
  69. },
  70. [
  71. isLoadingOrganizations,
  72. isErrorOrganizations,
  73. organizationsError,
  74. organizations,
  75. organization_slug,
  76. ]
  77. )
  78. return organizationsState
  79. }
  80. function usePrefillFormOnOrganizationsSuccess(
  81. form: UseFormReturn<IApprovalFormSchema>,
  82. organizationsState: ReturnType<typeof useOrganizationsState>,
  83. organization_slug: string | undefined
  84. ) {
  85. const prefillForm = useStaticEffectEvent(() => {
  86. if (organizationsState._tag === 'success') {
  87. preselectOrganizationSlug({
  88. form,
  89. organization_slug,
  90. organizations: organizationsState.organizations,
  91. })
  92. }
  93. })
  94. useEffect(() => {
  95. if (organizationsState._tag === 'success') {
  96. prefillForm()
  97. }
  98. }, [organizationsState._tag, prefillForm])
  99. }
  100. export interface ApiAuthorizationValidScreenProps {
  101. auth_id: string
  102. organization_slug: string | undefined
  103. navigate: (destination: string) => void
  104. }
  105. export function ApiAuthorizationValidScreen({
  106. auth_id,
  107. organization_slug,
  108. navigate,
  109. }: ApiAuthorizationValidScreenProps): ReactNode {
  110. const [approvalState, setApprovalState] = useState<ApprovalState>('indeterminate')
  111. const form = useForm<IApprovalFormSchema>({
  112. resolver: zodResolver(approvalFormSchema as any),
  113. defaultValues: { selectedOrgSlug: '' },
  114. mode: 'onSubmit',
  115. reValidateMode: 'onBlur',
  116. })
  117. const organizationsState = useOrganizationsState(organization_slug)
  118. usePrefillFormOnOrganizationsSuccess(form, organizationsState, organization_slug)
  119. const {
  120. data: requester,
  121. isPending: isLoading,
  122. isError,
  123. error,
  124. } = useApiAuthorizationQuery({ id: auth_id })
  125. const isApproved = (requester?.approved_at ?? null) !== null
  126. const { mutate: approveRequest } = useApiAuthorizationApproveMutation({
  127. onSuccess: (res) => {
  128. window.location.href = res.url
  129. },
  130. })
  131. const { mutate: declineRequest } = useApiAuthorizationDeclineMutation({
  132. onSuccess: () => {
  133. toast.success('Declined API authorization request')
  134. navigate('/organizations')
  135. },
  136. })
  137. const onApproveRequest = form.handleSubmit((values) => {
  138. if (approvalState !== 'indeterminate') {
  139. return
  140. }
  141. setApprovalState('approving')
  142. approveRequest(
  143. { id: auth_id, slug: values.selectedOrgSlug },
  144. { onError: () => setApprovalState('indeterminate') }
  145. )
  146. })
  147. const onDeclineRequest = form.handleSubmit((values) => {
  148. if (approvalState !== 'indeterminate') {
  149. return
  150. }
  151. setApprovalState('declining')
  152. declineRequest(
  153. { id: auth_id, slug: values.selectedOrgSlug },
  154. { onError: () => setApprovalState('indeterminate') }
  155. )
  156. })
  157. if (isLoading) {
  158. return <ApiAuthorizationLoadingScreen />
  159. }
  160. if (isError) {
  161. return <ApiAuthorizationErrorScreen error={error} />
  162. }
  163. if (isApproved) {
  164. const approvedOrganization =
  165. organizationsState._tag === 'success'
  166. ? organizationsState.organizations.find(
  167. (org) => org.slug === requester.approved_organization_slug
  168. )
  169. : undefined
  170. return (
  171. <ApiAuthorizationApprovedScreen requester={requester} organization={approvedOrganization} />
  172. )
  173. }
  174. return (
  175. <ApiAuthorizationMainView
  176. approvalState={approvalState}
  177. form={form}
  178. requester={requester}
  179. requestedOrganizationSlug={organization_slug}
  180. organizations={organizationsState}
  181. onApprove={onApproveRequest}
  182. onDecline={onDeclineRequest}
  183. />
  184. )
  185. }