Skip to content

Commit aa3cf18

Browse files
rahulchhabriaclaude
andcommitted
feat(admin): Replace startup program notes field with program dropdown
Replace the free-text notes field in the "Add to Startup Program" modal with a dropdown of predefined program options. Selecting "Other" reveals a custom notes text field for free-form input. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 4ea4154 commit aa3cf18

File tree

2 files changed

+105
-15
lines changed

2 files changed

+105
-15
lines changed

static/gsAdmin/components/addToStartupProgramAction.spec.tsx

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,14 @@ describe('AddToStartupProgramAction', () => {
5757
expect(await screen.findByTestId('balance')).toHaveTextContent('$30.00 owed');
5858
});
5959

60-
it('has default values for credit amount and notes', async () => {
60+
it('has default values for credit amount and program', async () => {
6161
triggerAddToStartupProgramModal(modalProps);
6262

6363
renderGlobalModal();
6464
expect(await screen.findByRole('spinbutton', {name: 'Credit Amount'})).toHaveValue(
6565
5000
6666
);
67-
expect(screen.getByRole('textbox', {name: 'Notes'})).toHaveValue('sentryforstartups');
67+
expect(screen.getByText('Sentry for Startups')).toBeInTheDocument();
6868
});
6969

7070
it('can submit with default values', async () => {
@@ -97,7 +97,57 @@ describe('AddToStartupProgramAction', () => {
9797
expect(onSuccess).toHaveBeenCalled();
9898
});
9999

100-
it('can submit with custom values', async () => {
100+
it('can submit with a different program selected', async () => {
101+
const updateMock = MockApiClient.addMockResponse({
102+
url: `/_admin/customers/${organization.slug}/balance-changes/`,
103+
method: 'POST',
104+
body: {},
105+
});
106+
107+
triggerAddToStartupProgramModal(modalProps);
108+
109+
const {waitForModalToHide} = renderGlobalModal();
110+
111+
// Change the program dropdown
112+
await userEvent.click(await screen.findByText('Sentry for Startups'));
113+
await userEvent.click(screen.getByText('Y Combinator'));
114+
115+
await userEvent.click(screen.getByRole('button', {name: 'Submit'}));
116+
117+
await waitForModalToHide();
118+
119+
await waitFor(() => {
120+
expect(updateMock).toHaveBeenCalledWith(
121+
`/_admin/customers/${organization.slug}/balance-changes/`,
122+
expect.objectContaining({
123+
method: 'POST',
124+
data: {
125+
creditAmount: 500000,
126+
ticketUrl: '',
127+
notes: 'ycombinator',
128+
},
129+
})
130+
);
131+
});
132+
});
133+
134+
it('shows custom notes field when "Other" is selected', async () => {
135+
triggerAddToStartupProgramModal(modalProps);
136+
137+
renderGlobalModal();
138+
139+
// Custom notes should not be visible initially
140+
expect(screen.queryByRole('textbox', {name: 'Custom Notes'})).not.toBeInTheDocument();
141+
142+
// Select "Other"
143+
await userEvent.click(await screen.findByText('Sentry for Startups'));
144+
await userEvent.click(screen.getByText('Other'));
145+
146+
// Custom notes field should now be visible
147+
expect(screen.getByRole('textbox', {name: 'Custom Notes'})).toBeInTheDocument();
148+
});
149+
150+
it('can submit with custom notes when "Other" is selected', async () => {
101151
const updateMock = MockApiClient.addMockResponse({
102152
url: `/_admin/customers/${organization.slug}/balance-changes/`,
103153
method: 'POST',
@@ -115,9 +165,14 @@ describe('AddToStartupProgramAction', () => {
115165

116166
await userEvent.type(screen.getByRole('textbox', {name: 'Ticket URL'}), url);
117167

118-
const notesInput = screen.getByRole('textbox', {name: 'Notes'});
119-
await userEvent.clear(notesInput);
120-
await userEvent.type(notesInput, 'custom note');
168+
// Select "Other" to show custom notes
169+
await userEvent.click(screen.getByText('Sentry for Startups'));
170+
await userEvent.click(screen.getByText('Other'));
171+
172+
await userEvent.type(
173+
screen.getByRole('textbox', {name: 'Custom Notes'}),
174+
'custom note'
175+
);
121176

122177
await userEvent.click(screen.getByRole('button', {name: 'Submit'}));
123178

@@ -156,7 +211,6 @@ describe('AddToStartupProgramAction', () => {
156211
expect(submitButton).toBeDisabled();
157212
expect(screen.getByRole('spinbutton', {name: 'Credit Amount'})).toBeDisabled();
158213
expect(screen.getByRole('textbox', {name: 'Ticket URL'})).toBeDisabled();
159-
expect(screen.getByRole('textbox', {name: 'Notes'})).toBeDisabled();
160214
await waitForModalToHide();
161215
});
162216

@@ -205,7 +259,6 @@ describe('AddToStartupProgramAction', () => {
205259
{timeout: 5_000}
206260
);
207261
expect(screen.getByRole('textbox', {name: 'Ticket URL'})).toBeEnabled();
208-
expect(screen.getByRole('textbox', {name: 'Notes'})).toBeEnabled();
209262
expect(screen.getByRole('button', {name: /submit/i})).toBeEnabled();
210263
}, 25_000);
211264

static/gsAdmin/components/addToStartupProgramAction.tsx

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Fragment} from 'react';
1+
import {Fragment, useState} from 'react';
22

33
import {Flex} from '@sentry/scraps/layout';
44
import {Heading, Text} from '@sentry/scraps/text';
@@ -8,6 +8,7 @@ import type {ModalRenderProps} from 'sentry/actionCreators/modal';
88
import {openModal} from 'sentry/actionCreators/modal';
99
import {InputField} from 'sentry/components/forms/fields/inputField';
1010
import {NumberField} from 'sentry/components/forms/fields/numberField';
11+
import {SelectField} from 'sentry/components/forms/fields/selectField';
1112
import {TextField} from 'sentry/components/forms/fields/textField';
1213
import {Form, type FormProps} from 'sentry/components/forms/form';
1314
import {fetchMutation, useMutation} from 'sentry/utils/queryClient';
@@ -16,6 +17,20 @@ import type {RequestError} from 'sentry/utils/requestError/requestError';
1617
import type {Subscription} from 'getsentry/types';
1718
import {formatBalance} from 'getsentry/utils/billing';
1819

20+
const STARTUP_PROGRAM_OPTIONS = [
21+
{value: 'ycombinator', label: 'Y Combinator'},
22+
{value: 'sentryforstartups', label: 'Sentry for Startups'},
23+
{value: 'a16z', label: 'a16z'},
24+
{value: 'accelatoms', label: 'Accelatoms'},
25+
{value: 'accelfam', label: 'Accelfam'},
26+
{value: 'renderstack', label: 'Renderstack'},
27+
{value: 'finpack', label: 'Finpack'},
28+
{value: 'betaworks', label: 'Betaworks'},
29+
{value: 'alchemist', label: 'Alchemist'},
30+
{value: 'antler', label: 'Antler'},
31+
{value: 'other', label: 'Other'},
32+
];
33+
1934
function coerceValue(value: number) {
2035
if (isNaN(value)) {
2136
return undefined;
@@ -46,6 +61,8 @@ function AddToStartupProgramModal({
4661
Header,
4762
Body,
4863
}: AddToStartupProgramModalProps) {
64+
const [showCustomNotes, setShowCustomNotes] = useState(false);
65+
4966
const {mutate, isPending} = useMutation<
5067
Record<string, any>,
5168
RequestError,
@@ -82,7 +99,13 @@ function AddToStartupProgramModal({
8299
const creditAmountInput = Number(data.creditAmount);
83100
const creditAmount = coerceValue(creditAmountInput);
84101
const ticketUrl = typeof data.ticketUrl === 'string' ? data.ticketUrl : '';
85-
const notes = typeof data.notes === 'string' ? data.notes : '';
102+
const notesProgram = typeof data.notesProgram === 'string' ? data.notesProgram : '';
103+
const notes =
104+
notesProgram === 'other'
105+
? typeof data.notesCustom === 'string'
106+
? data.notesCustom
107+
: ''
108+
: notesProgram;
86109

87110
if (!creditAmount || isPending) {
88111
return;
@@ -118,7 +141,8 @@ function AddToStartupProgramModal({
118141
footerClass="modal-footer"
119142
initialData={{
120143
creditAmount: 5000,
121-
notes: 'sentryforstartups',
144+
notesProgram: 'sentryforstartups',
145+
notesCustom: '',
122146
}}
123147
>
124148
<Flex direction="column" gap="md">
@@ -139,14 +163,27 @@ function AddToStartupProgramModal({
139163
stacked
140164
disabled={isPending}
141165
/>
142-
<TextField
143-
name="notes"
144-
label="Notes"
166+
<SelectField
167+
name="notesProgram"
168+
label="Program"
169+
options={STARTUP_PROGRAM_OPTIONS}
145170
inline={false}
146171
stacked
147-
maxLength={500}
148172
disabled={isPending}
173+
onChange={value => {
174+
setShowCustomNotes(value === 'other');
175+
}}
149176
/>
177+
{showCustomNotes && (
178+
<TextField
179+
name="notesCustom"
180+
label="Custom Notes"
181+
inline={false}
182+
stacked
183+
maxLength={500}
184+
disabled={isPending}
185+
/>
186+
)}
150187
</div>
151188
</Flex>
152189
</Form>

0 commit comments

Comments
 (0)