@@ -30,6 +30,7 @@ import { WizardAnswerGrid } from "./wizard-answer-grid"
3030import { WizardConfirmationDialog } from "./wizard-confirmation-dialog"
3131
3232const suffixSteps = getSuffixSteps ( )
33+ const buildStepSignature = ( step : WizardStep ) => `${ step . id } ::${ step . questions . map ( ( question ) => question . id ) . join ( "|" ) } `
3334
3435export function InstructionsWizard ( {
3536 initialStackId,
@@ -46,6 +47,7 @@ export function InstructionsWizard({
4647 initialStackId ? { [ STACK_QUESTION_ID ] : initialStackId } : { }
4748 )
4849 const [ freeTextResponses , setFreeTextResponses ] = useState < FreeTextResponses > ( { } )
50+ const [ freeTextDrafts , setFreeTextDrafts ] = useState < Record < string , string > > ( { } )
4951 const [ dynamicSteps , setDynamicSteps ] = useState < WizardStep [ ] > ( ( ) =>
5052 initialStackStep ? [ initialStackStep ] : [ ]
5153 )
@@ -65,6 +67,9 @@ export function InstructionsWizard({
6567 const hasAppliedInitialStack = useRef < string | null > (
6668 initialStackStep && initialStackId ? initialStackId : null
6769 )
70+ const lastAppliedStackStepSignature = useRef < string | null > (
71+ initialStackStep ? buildStepSignature ( initialStackStep ) : null
72+ )
6873 const [ activeStackLabel , setActiveStackLabel ] = useState < string | null > ( initialStackLabel )
6974
7075 const wizardSteps = useMemo ( ( ) => [ stacksStep , ...dynamicSteps , ...suffixSteps ] , [ dynamicSteps ] )
@@ -87,20 +92,32 @@ export function InstructionsWizard({
8792 const filterInputId = currentQuestion ? `answer-filter-${ currentQuestion . id } ` : "answer-filter"
8893
8994 const currentAnswerValue = currentQuestion ? responses [ currentQuestion . id ] : undefined
90- const currentFreeTextValue = useMemo ( ( ) => {
95+ const savedFreeTextValue = useMemo ( ( ) => {
9196 if ( ! currentQuestion ) {
9297 return ""
9398 }
9499
95100 const value = freeTextResponses [ currentQuestion . id ]
96101 return typeof value === "string" ? value : ""
97102 } , [ currentQuestion , freeTextResponses ] )
103+
104+ const draftFreeTextValue = useMemo ( ( ) => {
105+ if ( ! currentQuestion ) {
106+ return ""
107+ }
108+
109+ const value = freeTextDrafts [ currentQuestion . id ]
110+ return typeof value === "string" ? value : ""
111+ } , [ currentQuestion , freeTextDrafts ] )
112+
113+ const currentFreeTextValue =
114+ draftFreeTextValue . length > 0 ? draftFreeTextValue : savedFreeTextValue
98115 const freeTextConfig = currentQuestion ?. freeText ?? null
99116 const freeTextInputId = currentQuestion ? `free-text-${ currentQuestion . id } ` : "free-text"
100117 const canSubmitFreeText = Boolean ( freeTextConfig ?. enabled && currentFreeTextValue . trim ( ) . length > 0 )
101- const hasSavedCustomFreeText = Boolean ( freeTextConfig ?. enabled && currentFreeTextValue . trim ( ) . length > 0 )
118+ const hasSavedCustomFreeText = Boolean ( freeTextConfig ?. enabled && savedFreeTextValue . trim ( ) . length > 0 )
102119
103- const savedCustomFreeTextValue = currentFreeTextValue . trim ( )
120+ const savedCustomFreeTextValue = savedFreeTextValue . trim ( )
104121
105122 const defaultAnswer = useMemo (
106123 ( ) => currentQuestion ?. answers . find ( ( answer ) => answer . isDefault ) ?? null ,
@@ -190,25 +207,47 @@ export function InstructionsWizard({
190207 const applyStackStep = useCallback (
191208 ( step : WizardStep , label : string | null , options ?: { skipFastTrackPrompt ?: boolean ; stackId ?: string } ) => {
192209 const skipFastTrackPrompt = options ?. skipFastTrackPrompt ?? false
193- const nextStackId = options ?. stackId
210+ const nextStackId = options ?. stackId ?? null
211+ const stepSignature = buildStepSignature ( step )
212+ const previousSignature = lastAppliedStackStepSignature . current
213+ const isSameStep = previousSignature === stepSignature
194214
195215 setActiveStackLabel ( label )
196- setDynamicSteps ( [ step ] )
197216 setIsStackFastTrackPromptVisible ( ! skipFastTrackPrompt && step . questions . length > 0 )
198217
218+ if ( isSameStep ) {
219+ if ( nextStackId ) {
220+ setResponses ( ( prev ) => {
221+ if ( prev [ STACK_QUESTION_ID ] === nextStackId ) {
222+ return prev
223+ }
224+
225+ return {
226+ ...prev ,
227+ [ STACK_QUESTION_ID ] : nextStackId ,
228+ }
229+ } )
230+ }
231+
232+ lastAppliedStackStepSignature . current = stepSignature
233+ return
234+ }
235+
236+ const questionIds = new Set ( step . questions . map ( ( question ) => question . id ) )
237+
238+ setDynamicSteps ( [ step ] )
239+
199240 setResponses ( ( prev ) => {
200241 const next : Responses = { ...prev }
201242
202243 if ( nextStackId ) {
203244 next [ STACK_QUESTION_ID ] = nextStackId
204245 }
205246
206- step . questions . forEach ( ( question ) => {
207- if ( question . id === STACK_QUESTION_ID ) {
208- return
247+ questionIds . forEach ( ( questionId ) => {
248+ if ( questionId !== STACK_QUESTION_ID && questionId in next ) {
249+ delete next [ questionId ]
209250 }
210-
211- delete next [ question . id ]
212251 } )
213252
214253 return next
@@ -222,13 +261,27 @@ export function InstructionsWizard({
222261 let didMutate = false
223262 const next = { ...prev }
224263
225- step . questions . forEach ( ( question ) => {
226- if ( question . id === STACK_QUESTION_ID ) {
227- return
264+ questionIds . forEach ( ( questionId ) => {
265+ if ( questionId !== STACK_QUESTION_ID && questionId in next ) {
266+ delete next [ questionId ]
267+ didMutate = true
228268 }
269+ } )
270+
271+ return didMutate ? next : prev
272+ } )
273+
274+ setFreeTextDrafts ( ( prev ) => {
275+ if ( Object . keys ( prev ) . length === 0 ) {
276+ return prev
277+ }
278+
279+ let didMutate = false
280+ const next = { ...prev }
229281
230- if ( next [ question . id ] !== undefined ) {
231- delete next [ question . id ]
282+ questionIds . forEach ( ( questionId ) => {
283+ if ( questionId !== STACK_QUESTION_ID && questionId in next ) {
284+ delete next [ questionId ]
232285 didMutate = true
233286 }
234287 } )
@@ -239,6 +292,8 @@ export function InstructionsWizard({
239292 setCurrentStepIndex ( 1 )
240293 setCurrentQuestionIndex ( 0 )
241294 setAutoFilledQuestionMap ( { } )
295+
296+ lastAppliedStackStepSignature . current = stepSignature
242297 } ,
243298 [ ]
244299 )
@@ -415,6 +470,7 @@ export function InstructionsWizard({
415470 } )
416471
417472 setFreeTextResponses ( ( prev ) => ( Object . keys ( prev ) . length > 0 ? { } : prev ) )
473+ setFreeTextDrafts ( ( prev ) => ( Object . keys ( prev ) . length > 0 ? { } : prev ) )
418474
419475 markQuestionsAutoFilled ( autoFilledIds )
420476 setIsStackFastTrackPromptVisible ( false )
@@ -517,7 +573,7 @@ export function InstructionsWizard({
517573 const { value } = event . target
518574 let didChange = false
519575
520- setFreeTextResponses ( ( prev ) => {
576+ setFreeTextDrafts ( ( prev ) => {
521577 const existing = prev [ question . id ]
522578
523579 if ( value . length === 0 ) {
@@ -569,7 +625,9 @@ export function InstructionsWizard({
569625 ) => {
570626 const allowAutoAdvance = options ?. allowAutoAdvance ?? true
571627 const trimmedValue = rawValue . trim ( )
572- const existingValue = typeof freeTextResponses [ question . id ] === "string" ? freeTextResponses [ question . id ] : ""
628+ const draftValue = typeof freeTextDrafts [ question . id ] === "string" ? freeTextDrafts [ question . id ] : ""
629+ const savedValue = typeof freeTextResponses [ question . id ] === "string" ? freeTextResponses [ question . id ] : ""
630+ const existingValue = savedValue
573631
574632 if ( trimmedValue === existingValue ) {
575633 if ( allowAutoAdvance && trimmedValue . length > 0 && ! hasSelectionForQuestion ( question ) ) {
@@ -578,6 +636,18 @@ export function InstructionsWizard({
578636 } , 0 )
579637 }
580638
639+ if ( draftValue . length > 0 ) {
640+ setFreeTextDrafts ( ( prev ) => {
641+ if ( ! ( question . id in prev ) ) {
642+ return prev
643+ }
644+
645+ const next = { ...prev }
646+ delete next [ question . id ]
647+ return next
648+ } )
649+ }
650+
581651 return
582652 }
583653
@@ -598,6 +668,26 @@ export function InstructionsWizard({
598668 }
599669 } )
600670
671+ setFreeTextDrafts ( ( prev ) => {
672+ if ( trimmedValue . length === 0 ) {
673+ if ( ! ( question . id in prev ) ) {
674+ return prev
675+ }
676+
677+ const next = { ...prev }
678+ delete next [ question . id ]
679+ return next
680+ }
681+
682+ if ( ! ( question . id in prev ) ) {
683+ return prev
684+ }
685+
686+ const next = { ...prev }
687+ delete next [ question . id ]
688+ return next
689+ } )
690+
601691 clearAutoFilledFlag ( question . id )
602692
603693 if ( allowAutoAdvance && trimmedValue . length > 0 && ! hasSelectionForQuestion ( question ) ) {
@@ -660,6 +750,7 @@ export function InstructionsWizard({
660750 const stackIdToClear = selectedStackId
661751 setResponses ( { } )
662752 setFreeTextResponses ( { } )
753+ setFreeTextDrafts ( { } )
663754 setDynamicSteps ( [ ] )
664755 setCurrentStepIndex ( 0 )
665756 setCurrentQuestionIndex ( 0 )
@@ -856,6 +947,7 @@ export function InstructionsWizard({
856947 < form
857948 className = "flex flex-col gap-2 sm:flex-row sm:items-center"
858949 onSubmit = { handleFreeTextSubmit }
950+ data-testid = "wizard-free-text-form"
859951 >
860952 < Input
861953 id = { freeTextInputId }
@@ -866,7 +958,13 @@ export function InstructionsWizard({
866958 autoComplete = "off"
867959 />
868960 < div className = "flex gap-2" >
869- < Button type = "submit" size = "sm" disabled = { ! canSubmitFreeText } >
961+ < Button
962+ type = "submit"
963+ size = "sm"
964+ disabled = { ! canSubmitFreeText }
965+ data-can-submit = { canSubmitFreeText ? "true" : "false" }
966+ data-free-text-length = { currentFreeTextValue . length }
967+ >
870968 Save custom answer
871969 </ Button >
872970 { currentFreeTextValue . length > 0 ? (
0 commit comments