Skip to content

Commit c196e71

Browse files
feat(admin): Replace startup program notes field with program dropdown (#111701)
Replace the free-text notes field in the "Add to Startup Program" admin modal with a dropdown of predefined program options: - Y Combinator - Sentry for Startups (default) - a16z - Accelatoms - Accelfam - Renderstack - Finpack - Betaworks - Alchemist - Antler - Other (reveals a free-text input) When "Other" is selected, a custom notes text field appears for free-form input. The submitted `notes` value is the selected program key, or the custom text when "Other" is chosen. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9b16dfe commit c196e71

File tree

2 files changed

+111
-11
lines changed

2 files changed

+111
-11
lines changed

static/gsAdmin/components/addToStartupProgramAction.spec.tsx

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ describe('AddToStartupProgramAction', () => {
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('sentryforstartups')).toBeInTheDocument();
6868
});
6969

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

100-
it('can submit with custom values', async () => {
100+
it('can submit with a different option 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+
await userEvent.click(await screen.findByText('sentryforstartups'));
112+
await userEvent.click(screen.getByText('ycombinator'));
113+
114+
await userEvent.click(screen.getByRole('button', {name: 'Submit'}));
115+
116+
await waitForModalToHide();
117+
118+
await waitFor(() => {
119+
expect(updateMock).toHaveBeenCalledWith(
120+
`/_admin/customers/${organization.slug}/balance-changes/`,
121+
expect.objectContaining({
122+
method: 'POST',
123+
data: {
124+
creditAmount: 500000,
125+
ticketUrl: '',
126+
notes: 'ycombinator',
127+
},
128+
})
129+
);
130+
});
131+
});
132+
133+
it('shows custom notes field when "Enter custom notes" is selected', async () => {
134+
triggerAddToStartupProgramModal(modalProps);
135+
136+
renderGlobalModal();
137+
138+
expect(screen.queryByRole('textbox', {name: 'Custom Notes'})).not.toBeInTheDocument();
139+
140+
await userEvent.click(await screen.findByText('sentryforstartups'));
141+
await userEvent.click(screen.getByText('Enter custom notes'));
142+
143+
expect(screen.getByRole('textbox', {name: 'Custom Notes'})).toBeInTheDocument();
144+
});
145+
146+
it('hides custom notes field when switching back to a preset option', async () => {
147+
triggerAddToStartupProgramModal(modalProps);
148+
149+
renderGlobalModal();
150+
151+
// Select "Enter custom notes"
152+
await userEvent.click(await screen.findByText('sentryforstartups'));
153+
await userEvent.click(screen.getByText('Enter custom notes'));
154+
expect(screen.getByRole('textbox', {name: 'Custom Notes'})).toBeInTheDocument();
155+
156+
// Switch back to a preset option
157+
await userEvent.click(screen.getByText('Enter custom notes'));
158+
await userEvent.click(screen.getByText('a16z'));
159+
expect(screen.queryByRole('textbox', {name: 'Custom Notes'})).not.toBeInTheDocument();
160+
});
161+
162+
it('can submit with custom notes', async () => {
101163
const updateMock = MockApiClient.addMockResponse({
102164
url: `/_admin/customers/${organization.slug}/balance-changes/`,
103165
method: 'POST',
@@ -115,9 +177,13 @@ describe('AddToStartupProgramAction', () => {
115177

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

118-
const notesInput = screen.getByRole('textbox', {name: 'Notes'});
119-
await userEvent.clear(notesInput);
120-
await userEvent.type(notesInput, 'custom note');
180+
await userEvent.click(screen.getByText('sentryforstartups'));
181+
await userEvent.click(screen.getByText('Enter custom notes'));
182+
183+
await userEvent.type(
184+
screen.getByRole('textbox', {name: 'Custom Notes'}),
185+
'custom note'
186+
);
121187

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

@@ -156,7 +222,6 @@ describe('AddToStartupProgramAction', () => {
156222
expect(submitButton).toBeDisabled();
157223
expect(screen.getByRole('spinbutton', {name: 'Credit Amount'})).toBeDisabled();
158224
expect(screen.getByRole('textbox', {name: 'Ticket URL'})).toBeDisabled();
159-
expect(screen.getByRole('textbox', {name: 'Notes'})).toBeDisabled();
160225
await waitForModalToHide();
161226
});
162227

@@ -205,7 +270,6 @@ describe('AddToStartupProgramAction', () => {
205270
{timeout: 5_000}
206271
);
207272
expect(screen.getByRole('textbox', {name: 'Ticket URL'})).toBeEnabled();
208-
expect(screen.getByRole('textbox', {name: 'Notes'})).toBeEnabled();
209273
expect(screen.getByRole('button', {name: /submit/i})).toBeEnabled();
210274
}, 25_000);
211275

static/gsAdmin/components/addToStartupProgramAction.tsx

Lines changed: 40 additions & 4 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: 'ycombinator'},
22+
{value: 'sentryforstartups', label: 'sentryforstartups'},
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: 'Enter custom notes'},
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 rawNotes = typeof data.notes === 'string' ? data.notes : '';
103+
const notes =
104+
rawNotes === 'other'
105+
? typeof data.customNotes === 'string'
106+
? data.customNotes
107+
: ''
108+
: rawNotes;
86109

87110
if (!creditAmount || isPending) {
88111
return;
@@ -139,14 +162,27 @@ function AddToStartupProgramModal({
139162
stacked
140163
disabled={isPending}
141164
/>
142-
<TextField
165+
<SelectField
143166
name="notes"
144167
label="Notes"
168+
options={STARTUP_PROGRAM_OPTIONS}
145169
inline={false}
146170
stacked
147-
maxLength={500}
148171
disabled={isPending}
172+
onChange={value => {
173+
setShowCustomNotes(value === 'other');
174+
}}
149175
/>
176+
{showCustomNotes && (
177+
<TextField
178+
name="customNotes"
179+
label="Custom Notes"
180+
inline={false}
181+
stacked
182+
maxLength={500}
183+
disabled={isPending}
184+
/>
185+
)}
150186
</div>
151187
</Flex>
152188
</Form>

0 commit comments

Comments
 (0)