Skip to content

Commit 63dffeb

Browse files
committed
ref(onboarding): Persist project details form state in onboarding context
Add projectDetailsForm to the onboarding session state so the SCM project details step can restore the user's previous inputs (name, team, alert config) when navigating back from setup-docs. This mirrors the pattern used by createProject.tsx which persists form state to localStorage for the same purpose.
1 parent 101823f commit 63dffeb

File tree

3 files changed

+123
-6
lines changed

3 files changed

+123
-6
lines changed

static/app/components/onboarding/onboardingContext.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,33 @@ import {createContext, useContext, useMemo} from 'react';
33
import type {ProductSolution} from 'sentry/components/onboarding/gettingStartedDoc/types';
44
import type {Integration, Repository} from 'sentry/types/integrations';
55
import type {OnboardingSelectedSDK} from 'sentry/types/onboarding';
6+
import type {PlatformKey} from 'sentry/types/project';
67
import {useSessionStorage} from 'sentry/utils/useSessionStorage';
8+
import type {AlertRuleOptions} from 'sentry/views/projectInstall/issueAlertOptions';
9+
10+
/**
11+
* Persisted form state from the SCM project details step. Stored so the
12+
* form can be restored when the user navigates back from setup-docs.
13+
* `platform` records the platform at creation time so we can detect
14+
* platform changes when the user navigates back through earlier steps.
15+
*/
16+
export interface ProjectDetailsFormState {
17+
alertRuleConfig?: AlertRuleOptions;
18+
platform?: PlatformKey;
19+
projectName?: string;
20+
teamSlug?: string;
21+
}
722

823
type OnboardingContextProps = {
924
clearDerivedState: () => void;
1025
setCreatedProjectSlug: (slug?: string) => void;
26+
setProjectDetailsForm: (form?: ProjectDetailsFormState) => void;
1127
setSelectedFeatures: (features?: ProductSolution[]) => void;
1228
setSelectedIntegration: (integration?: Integration) => void;
1329
setSelectedPlatform: (selectedSDK?: OnboardingSelectedSDK) => void;
1430
setSelectedRepository: (repo?: Repository) => void;
1531
createdProjectSlug?: string;
32+
projectDetailsForm?: ProjectDetailsFormState;
1633
selectedFeatures?: ProductSolution[];
1734
selectedIntegration?: Integration;
1835
selectedPlatform?: OnboardingSelectedSDK;
@@ -21,6 +38,7 @@ type OnboardingContextProps = {
2138

2239
export type OnboardingSessionState = {
2340
createdProjectSlug?: string;
41+
projectDetailsForm?: ProjectDetailsFormState;
2442
selectedFeatures?: ProductSolution[];
2543
selectedIntegration?: Integration;
2644
selectedPlatform?: OnboardingSelectedSDK;
@@ -41,6 +59,8 @@ const OnboardingContext = createContext<OnboardingContextProps>({
4159
setSelectedFeatures: () => {},
4260
createdProjectSlug: undefined,
4361
setCreatedProjectSlug: () => {},
62+
projectDetailsForm: undefined,
63+
setProjectDetailsForm: () => {},
4464
clearDerivedState: () => {},
4565
});
4666

@@ -84,6 +104,10 @@ export function OnboardingContextProvider({children, initialValue}: ProviderProp
84104
setCreatedProjectSlug: (createdProjectSlug?: string) => {
85105
setOnboarding(prev => ({...prev, createdProjectSlug}));
86106
},
107+
projectDetailsForm: onboarding?.projectDetailsForm,
108+
setProjectDetailsForm: (projectDetailsForm?: ProjectDetailsFormState) => {
109+
setOnboarding(prev => ({...prev, projectDetailsForm}));
110+
},
87111
// Clear state derived from the selected repository (platform, features,
88112
// created project) without wiping the entire session. Use this when the
89113
// repo changes so downstream steps start fresh.
@@ -93,6 +117,7 @@ export function OnboardingContextProvider({children, initialValue}: ProviderProp
93117
selectedPlatform: undefined,
94118
selectedFeatures: undefined,
95119
createdProjectSlug: undefined,
120+
projectDetailsForm: undefined,
96121
}));
97122
},
98123
}),

static/app/views/onboarding/scmProjectDetails.spec.tsx

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,82 @@ describe('ScmProjectDetails', () => {
299299
expect(stored.selectedPlatform?.key).toBe('javascript-nextjs');
300300
});
301301

302+
it('restores form inputs from persisted projectDetailsForm', async () => {
303+
render(
304+
<ScmProjectDetails
305+
onComplete={jest.fn()}
306+
stepIndex={3}
307+
genSkipOnboardingLink={() => null}
308+
/>,
309+
{
310+
organization,
311+
additionalWrapper: makeOnboardingWrapper({
312+
selectedPlatform: mockPlatform,
313+
projectDetailsForm: {
314+
projectName: 'my-saved-name',
315+
teamSlug: teamWithAccess.slug,
316+
platform: 'javascript-nextjs',
317+
},
318+
}),
319+
}
320+
);
321+
322+
const input = await screen.findByPlaceholderText('project-name');
323+
expect(input).toHaveValue('my-saved-name');
324+
});
325+
326+
it('persists form state to context on successful creation', async () => {
327+
MockApiClient.addMockResponse({
328+
url: `/teams/${organization.slug}/${teamWithAccess.slug}/projects/`,
329+
method: 'POST',
330+
body: ProjectFixture({slug: 'javascript-nextjs', name: 'javascript-nextjs'}),
331+
});
332+
MockApiClient.addMockResponse({
333+
url: `/organizations/${organization.slug}/`,
334+
body: organization,
335+
});
336+
MockApiClient.addMockResponse({
337+
url: `/organizations/${organization.slug}/projects/`,
338+
body: [],
339+
});
340+
MockApiClient.addMockResponse({
341+
url: `/organizations/${organization.slug}/teams/`,
342+
body: [teamWithAccess],
343+
});
344+
345+
const onComplete = jest.fn();
346+
347+
render(
348+
<ScmProjectDetails
349+
onComplete={onComplete}
350+
stepIndex={3}
351+
genSkipOnboardingLink={() => null}
352+
/>,
353+
{
354+
organization,
355+
additionalWrapper: makeOnboardingWrapper({
356+
selectedPlatform: mockPlatform,
357+
}),
358+
}
359+
);
360+
361+
await userEvent.click(await screen.findByRole('button', {name: 'Create project'}));
362+
363+
await waitFor(() => {
364+
expect(onComplete).toHaveBeenCalled();
365+
});
366+
367+
const stored = JSON.parse(sessionStorageWrapper.getItem('onboarding') ?? '{}');
368+
expect(stored.projectDetailsForm).toEqual(
369+
expect.objectContaining({
370+
projectName: 'javascript-nextjs',
371+
teamSlug: teamWithAccess.slug,
372+
platform: 'javascript-nextjs',
373+
})
374+
);
375+
expect(stored.projectDetailsForm.alertRuleConfig).toBeDefined();
376+
});
377+
302378
it('shows error message on project creation failure', async () => {
303379
const onComplete = jest.fn();
304380

static/app/views/onboarding/scmProjectDetails.tsx

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,13 @@ const PROJECT_DETAILS_WIDTH = '285px';
3333

3434
export function ScmProjectDetails({onComplete}: StepProps) {
3535
const organization = useOrganization();
36-
const {selectedPlatform, selectedFeatures, setCreatedProjectSlug} =
37-
useOnboardingContext();
36+
const {
37+
selectedPlatform,
38+
selectedFeatures,
39+
setCreatedProjectSlug,
40+
projectDetailsForm,
41+
setProjectDetailsForm,
42+
} = useOnboardingContext();
3843
const {teams} = useTeams();
3944
const createProjectAndRules = useCreateProjectAndRules();
4045
useEffect(() => {
@@ -44,15 +49,20 @@ export function ScmProjectDetails({onComplete}: StepProps) {
4449
const firstAdminTeam = teams.find((team: Team) => team.access.includes('team:admin'));
4550
const defaultName = slugify(selectedPlatform?.key ?? '');
4651

47-
// State tracks user edits; derived values fall back to defaults from context/teams
48-
const [projectName, setProjectName] = useState<string | null>(null);
49-
const [teamSlug, setTeamSlug] = useState<string | null>(null);
52+
// State tracks user edits. When the user navigates back from setup-docs
53+
// the persisted projectDetailsForm restores their previous inputs.
54+
const [projectName, setProjectName] = useState<string | null>(
55+
projectDetailsForm?.projectName ?? null
56+
);
57+
const [teamSlug, setTeamSlug] = useState<string | null>(
58+
projectDetailsForm?.teamSlug ?? null
59+
);
5060

5161
const projectNameResolved = projectName ?? defaultName;
5262
const teamSlugResolved = teamSlug ?? firstAdminTeam?.slug ?? '';
5363

5464
const [alertRuleConfig, setAlertRuleConfig] = useState<AlertRuleOptions>(
55-
DEFAULT_ISSUE_ALERT_OPTIONS_VALUES
65+
projectDetailsForm?.alertRuleConfig ?? DEFAULT_ISSUE_ALERT_OPTIONS_VALUES
5666
);
5767

5868
function handleAlertChange<K extends keyof AlertRuleOptions>(
@@ -116,6 +126,12 @@ export function ScmProjectDetails({onComplete}: StepProps) {
116126
// the project via useRecentCreatedProject without corrupting
117127
// selectedPlatform.key (which the platform features step needs).
118128
setCreatedProjectSlug(project.slug);
129+
setProjectDetailsForm({
130+
projectName: projectNameResolved,
131+
teamSlug: teamSlugResolved,
132+
alertRuleConfig,
133+
platform: selectedPlatform.key,
134+
});
119135

120136
trackAnalytics('onboarding.scm_project_details_create_succeeded', {
121137
organization,

0 commit comments

Comments
 (0)