1- import { useCallback , useState } from 'react' ;
1+ import { useCallback , useRef } from 'react' ;
22import * as Sentry from '@sentry/react' ;
33
44import { addErrorMessage } from 'sentry/actionCreators/indicator' ;
@@ -69,6 +69,13 @@ interface UseScmProjectDetailsOptions {
6969 * both and advance in one place.
7070 */
7171 onComplete : ( completion : ScmProjectDetailsCompletion ) => void ;
72+ /**
73+ * Live form state, owned by the host. Fields absent from the form derive
74+ * their defaults (platform-based name, first admin team, default alert
75+ * config), so the host clearing the form makes the fields re-derive.
76+ */
77+ onProjectDetailsFormChange : ( form : ProjectDetailsFormState ) => void ;
78+ projectDetailsForm : ProjectDetailsFormState | undefined ;
7279 selectedPlatform : OnboardingSelectedSDK | undefined ;
7380 selectedRepository : Repository | undefined ;
7481 /**
@@ -79,7 +86,6 @@ interface UseScmProjectDetailsOptions {
7986 allowMemberWithoutTeam ?: boolean ;
8087 /** Slug of an already-created project, used for the back-nav reuse check. */
8188 createdProjectSlug ?: string ;
82- projectDetailsForm ?: ProjectDetailsFormState ;
8389}
8490
8591interface ScmProjectDetailsForm {
@@ -108,19 +114,22 @@ interface ScmProjectDetailsForm {
108114}
109115
110116/**
111- * Owns the SCM project-details form state, the create-project + repo-link flow,
112- * and the field/create analytics (routed by `analyticsFlow`). Returned to the
113- * host so it can render the presentational `ScmProjectDetailsCore` form and
114- * place its own Create button (onboarding's fixed footer, project creation's
115- * page-level footer) independent of where the form fields render.
117+ * Drives the SCM project-details form (state owned by the host via
118+ * `projectDetailsForm`/`onProjectDetailsFormChange`), the create-project +
119+ * repo-link flow, and the field/create analytics (routed by `analyticsFlow`).
120+ * Returned to the host so it can render the presentational
121+ * `ScmProjectDetailsCore` form and place its own Create button (onboarding's
122+ * fixed footer, project creation's page-level footer) independent of where
123+ * the form fields render.
116124 */
117125export function useScmProjectDetails ( {
118126 analyticsFlow,
119127 onComplete,
128+ onProjectDetailsFormChange,
129+ projectDetailsForm,
120130 selectedPlatform,
121131 selectedRepository,
122132 createdProjectSlug,
123- projectDetailsForm,
124133 allowMemberWithoutTeam,
125134} : UseScmProjectDetailsOptions ) : ScmProjectDetailsForm {
126135 const organization = useOrganization ( ) ;
@@ -139,41 +148,48 @@ export function useScmProjectDetails({
139148 ! organization . access . includes ( 'project:admin' ) ;
140149 const defaultName = slugify ( selectedPlatform ?. key ?? '' ) ;
141150
142- // State tracks user edits. When the host restores a persisted
143- // projectDetailsForm (e.g. back-navigation in onboarding) it seeds the inputs.
144- const [ projectName , setProjectName ] = useState ( projectDetailsForm ?. projectName ?? null ) ;
145- const [ teamSlug , setTeamSlug ] = useState ( projectDetailsForm ?. teamSlug ?? null ) ;
146- const [ alertRuleConfig , setAlertRuleConfig ] = useState (
147- projectDetailsForm ?. alertRuleConfig ?? DEFAULT_ISSUE_ALERT_OPTIONS_VALUES
148- ) ;
151+ // Fields absent from the host-owned form fall back to derived defaults, so
152+ // a host clearing the form (e.g. on a platform change) re-derives them.
153+ const projectNameResolved = projectDetailsForm ?. projectName ?? defaultName ;
154+ const teamSlugResolved = projectDetailsForm ?. teamSlug ?? firstAdminTeam ?. slug ?? '' ;
155+ const alertRuleConfig =
156+ projectDetailsForm ?. alertRuleConfig ?? DEFAULT_ISSUE_ALERT_OPTIONS_VALUES ;
149157
150- const projectNameResolved = projectName ?? defaultName ;
151- const teamSlugResolved = teamSlug ?? firstAdminTeam ?. slug ?? '' ;
158+ // Baseline for the unchanged-return (reuse) check below: the form as it was
159+ // when this step mounted, i.e. a restored session's saved values. Live edits
160+ // flow through the controlled form, so the prop can't be its own baseline.
161+ const savedFormRef = useRef ( projectDetailsForm ) ;
152162
153- const onProjectNameChange = useCallback ( ( value : string ) => {
154- setProjectName ( slugify ( value ) ) ;
155- } , [ ] ) ;
163+ const onProjectNameChange = useCallback (
164+ ( value : string ) => {
165+ onProjectDetailsFormChange ( { ...projectDetailsForm , projectName : slugify ( value ) } ) ;
166+ } ,
167+ [ onProjectDetailsFormChange , projectDetailsForm ]
168+ ) ;
156169
157170 const onProjectNameBlur = useCallback ( ( ) => {
158- if ( projectName !== null ) {
171+ if ( projectDetailsForm ?. projectName !== undefined ) {
159172 trackAnalytics ( NAME_EDITED_EVENT [ analyticsFlow ] , {
160173 organization,
161- custom : projectName !== defaultName ,
174+ custom : projectDetailsForm . projectName !== defaultName ,
162175 } ) ;
163176 }
164- } , [ projectName , defaultName , organization , analyticsFlow ] ) ;
177+ } , [ projectDetailsForm ?. projectName , defaultName , organization , analyticsFlow ] ) ;
165178
166179 const onTeamChange = useCallback (
167180 ( { value} : { value : string } ) => {
168- setTeamSlug ( value ) ;
181+ onProjectDetailsFormChange ( { ... projectDetailsForm , teamSlug : value } ) ;
169182 trackAnalytics ( TEAM_SELECTED_EVENT [ analyticsFlow ] , { organization, team : value } ) ;
170183 } ,
171- [ organization , analyticsFlow ]
184+ [ onProjectDetailsFormChange , projectDetailsForm , organization , analyticsFlow ]
172185 ) ;
173186
174187 const onAlertChange = useCallback (
175188 < K extends keyof AlertRuleOptions > ( key : K , value : AlertRuleOptions [ K ] ) => {
176- setAlertRuleConfig ( prev => ( { ...prev , [ key ] : value } ) ) ;
189+ onProjectDetailsFormChange ( {
190+ ...projectDetailsForm ,
191+ alertRuleConfig : { ...alertRuleConfig , [ key ] : value } ,
192+ } ) ;
177193 if ( key === 'alertSetting' ) {
178194 const optionMap : Record < number , string > = {
179195 [ RuleAction . DEFAULT_ALERT ] : 'high_priority' ,
@@ -186,7 +202,13 @@ export function useScmProjectDetails({
186202 } ) ;
187203 }
188204 } ,
189- [ organization , analyticsFlow ]
205+ [
206+ onProjectDetailsFormChange ,
207+ projectDetailsForm ,
208+ alertRuleConfig ,
209+ organization ,
210+ analyticsFlow ,
211+ ]
190212 ) ;
191213
192214 // Block submission until teams and the projects store have loaded so the
@@ -207,12 +229,13 @@ export function useScmProjectDetails({
207229 // snapshot because the Project model tracks it; alert fields are not on the
208230 // Project record so we compare those against the context snapshot.
209231 const samePlatform = existingProject ?. platform === selectedPlatform ?. key ;
210- const savedAlert = projectDetailsForm ?. alertRuleConfig ;
232+ const savedForm = savedFormRef . current ;
233+ const savedAlert = savedForm ?. alertRuleConfig ;
211234 const nothingChanged =
212235 samePlatform &&
213- ! ! projectDetailsForm &&
214- projectNameResolved === projectDetailsForm . projectName &&
215- teamSlugResolved === projectDetailsForm . teamSlug &&
236+ ! ! savedForm &&
237+ projectNameResolved === savedForm . projectName &&
238+ teamSlugResolved === savedForm . teamSlug &&
216239 alertRuleConfig . alertSetting === savedAlert ?. alertSetting &&
217240 alertRuleConfig . interval === savedAlert ?. interval &&
218241 alertRuleConfig . metric === savedAlert ?. metric &&
0 commit comments