diff --git a/static/app/views/organizationJoinRequest/index.spec.tsx b/static/app/views/organizationJoinRequest/index.spec.tsx index a049fe049828..a2ea16e9a652 100644 --- a/static/app/views/organizationJoinRequest/index.spec.tsx +++ b/static/app/views/organizationJoinRequest/index.spec.tsx @@ -8,6 +8,7 @@ import OrganizationJoinRequest from 'sentry/views/organizationJoinRequest'; jest.mock('sentry/utils/analytics', () => ({ trackAdhocEvent: jest.fn(), + trackAnalytics: jest.fn(), })); jest.mock('sentry/actionCreators/indicator'); @@ -66,6 +67,32 @@ describe('OrganizationJoinRequest', () => { ).not.toBeInTheDocument(); }); + it('shows validation error for invalid email', async () => { + const postMock = MockApiClient.addMockResponse({ + url: endpoint, + method: 'POST', + }); + + render(, { + initialRouterConfig: { + location: { + pathname: `/join-request/${org.slug}/`, + }, + route: '/join-request/:orgId/', + }, + }); + + await userEvent.type( + screen.getByRole('textbox', {name: 'Email Address'}), + 'not-an-email{enter}' + ); + + expect( + await screen.findByText('Please enter a valid email address') + ).toBeInTheDocument(); + expect(postMock).not.toHaveBeenCalled(); + }); + it('errors', async () => { const postMock = MockApiClient.addMockResponse({ url: endpoint, diff --git a/static/app/views/organizationJoinRequest/index.tsx b/static/app/views/organizationJoinRequest/index.tsx index 088075f83351..ad42c9697cde 100644 --- a/static/app/views/organizationJoinRequest/index.tsx +++ b/static/app/views/organizationJoinRequest/index.tsx @@ -1,113 +1,121 @@ -import {useCallback, useState} from 'react'; import type {MouseEvent} from 'react'; -import styled from '@emotion/styled'; +import {useMutation} from '@tanstack/react-query'; +import {z} from 'zod'; + +import {Button} from '@sentry/scraps/button'; +import {defaultFormOptions, useScrapsForm} from '@sentry/scraps/form'; +import {Container, Flex, Stack} from '@sentry/scraps/layout'; +import {Heading, Text} from '@sentry/scraps/text'; import {addErrorMessage} from 'sentry/actionCreators/indicator'; -import {EmailField} from 'sentry/components/forms/fields/emailField'; -import {Form} from 'sentry/components/forms/form'; import {NarrowLayout} from 'sentry/components/narrowLayout'; import {IconMegaphone} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import {trackAnalytics} from 'sentry/utils/analytics'; +import {fetchMutation} from 'sentry/utils/queryClient'; import {decodeScalar} from 'sentry/utils/queryString'; import {testableWindowLocation} from 'sentry/utils/testableWindowLocation'; import {useLocation} from 'sentry/utils/useLocation'; import {useParams} from 'sentry/utils/useParams'; +const joinRequestSchema = z.object({ + email: z.email(t('Please enter a valid email address')), +}); + export default function OrganizationJoinRequest() { const {orgId} = useParams<{orgId: string}>(); const location = useLocation(); - const [submitSuccess, setSubmitSuccess] = useState(false); - const handleSubmitSuccess = useCallback(() => { - setSubmitSuccess(true); - trackAnalytics('join_request.created', { - organization: orgId, - referrer: decodeScalar(location.query.referrer, ''), - }); - }, [orgId, location.query.referrer]); + const mutation = useMutation({ + mutationFn: (data: {email: string}) => + fetchMutation({ + url: `/organizations/${orgId}/join-request/`, + method: 'POST', + data, + }), + onSuccess: () => { + trackAnalytics('join_request.created', { + organization: orgId, + referrer: decodeScalar(location.query.referrer, ''), + }); + }, + onError: () => { + addErrorMessage(t('Request to join failed')); + }, + }); - const handleSubmitError = useCallback(() => { - addErrorMessage(t('Request to join failed')); - }, []); + const form = useScrapsForm({ + ...defaultFormOptions, + defaultValues: {email: ''}, + validators: {onDynamic: joinRequestSchema}, + onSubmit: ({value}) => mutation.mutateAsync(value).catch(() => {}), + }); - const handleCancel = useCallback( - (e: MouseEvent) => { - e.preventDefault(); - testableWindowLocation.assign(`/auth/login/${orgId}/`); - }, - [orgId] - ); + const handleCancel = (e: MouseEvent) => { + e.preventDefault(); + testableWindowLocation.assign(`/auth/login/${orgId}/`); + }; - if (submitSuccess) { + if (mutation.isSuccess) { return ( - - - {t('Request Sent')} - {t('Your request to join has been sent.')} - - {t('You will receive an email when your request is approved.')} - - + + + + + {t('Request Sent')} + + + {t('Your request to join has been sent.')} + + + + {t('You will receive an email when your request is approved.')} + + + + ); } return ( - - {t('Request to Join')} - - {tct('Ask the admins if you can join the [orgId] organization.', { - orgId, - })} - -
- - + + + + + {t('Request to Join')} + + + {tct('Ask the admins if you can join the [orgId] organization.', { + orgId, + })} + + + + + + {field => ( + + + + )} + + + + + {t('Request to Join')} + + + + +
); } - -const SuccessModal = styled('div')` - display: grid; - justify-items: center; - text-align: center; - padding-top: 10px; - padding-bottom: ${p => p.theme.space['3xl']}; -`; - -const StyledIconMegaphone = styled(IconMegaphone)` - padding-bottom: ${p => p.theme.space['2xl']}; -`; - -const StyledHeader = styled('h3')` - margin-bottom: ${p => p.theme.space.md}; -`; - -const StyledText = styled('p')` - margin-bottom: 0; -`; - -const ReceiveEmailMessage = styled(StyledText)` - max-width: 250px; -`; - -const StyledEmailField = styled(EmailField)` - padding-top: ${p => p.theme.space.xl}; - padding-left: 0; -`;