Conversation
|
A preview of ee4ecc9 is uploaded and can be seen here: ✨ https://revisit.dev/study/PR1449 ✨ Changes may take a few minutes to propagate. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4ec7cf9b31
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| autoAdvanceTimeoutRef.current = setTimeout(() => { | ||
| handleNextClick(); | ||
| }, readyResponse.autoAdvanceDelay ?? 0); |
There was a problem hiding this comment.
Persist the latest selection when the delay expires
With autoAdvanceDelay and the default allowResponseChange: true, selecting A and then B before the timeout expires displays B but can persist A. The timeout closes over the handleNextClick/goToNextStep instance—and therefore trialValidation—from the render that armed it, while the identifier latch prevents later renders from replacing the callback. Invoke the latest callback through a ref or cancel and re-arm the timer when the answer changes.
Useful? React with 👍 / 👎.
| autoAdvanceIdentifierRef.current = identifier; | ||
| if (readyResponse.type === 'buttons') { | ||
| autoAdvanceTimeoutRef.current = setTimeout(() => { | ||
| handleNextClick(); |
There was a problem hiding this comment.
Keep auto-advance behind the training gate
For a buttons component with provideFeedback and correctAnswer, the normal Next button is disabled until Check Answer records a correct response or exhausts the configured attempts, but this direct call advances after any ordinarily valid selection. An incorrect answer therefore skips feedback and attempt tracking entirely; disable auto-advance for feedback components or route it through the same Check Answer state machine.
Useful? React with 👍 / 👎.
| return; | ||
| } | ||
|
|
||
| const readyResponse = autoAdvanceResponses.find((response) => hasAnswerValue(combinedValues[response.id])); |
There was a problem hiding this comment.
Trigger auto-advance only from a new selection
This value-based condition also matches answers that were not entered during the current visit. A configured buttons default skips the step without participant interaction, and a persisted answer restored after the participant clicks Previous immediately sends them forward again, preventing review or editing even when backward navigation is enabled. Arm auto-advance only on a new participant selection rather than on any nonempty initialized value.
Useful? React with 👍 / 👎.
| if (!component.response || !Array.isArray(component.response)) { | ||
| return; |
There was a problem hiding this comment.
Validate inherited responses before issuing warnings
When a component declares only baseComponent, this early return skips the warning even if the resolved base component contains an auto-advance button plus another required response. Runtime rendering merges that base through studyComponentToIndividualComponent, so the unsafe multi-response configuration is accepted without the warning introduced here. Run this check against the resolved component instead of the raw override.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 66193a3f41
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const prevValuesRef = useRef(answerValidator.values); | ||
| useEffect(() => { | ||
| if (!isEqual(prevValuesRef.current, answerValidator.values)) { | ||
| if (Object.keys(answerValidator.values).length > 0) { | ||
| userInteractedRef.current = true; |
There was a problem hiding this comment.
Track interactions from the auto-advance response block
When an auto-advance button is in sidebar or aboveStimulus while nextButtonLocation is elsewhere, auto-advance never fires. Only the ResponseBlock containing the Next button becomes autoAdvanceOwner, but this interaction flag watches that block's location-filtered answerValidator.values; the block containing the clicked button sees the interaction but is not the owner, while the owner sees the shared answer without setting this flag. Track the auto-advance response's transition in combinedValues or propagate its input event to the owner.
Useful? React with 👍 / 👎.
| if (response.allowResponseChange === false) { | ||
| return hasAnswerValue(ans.value); |
There was a problem hiding this comment.
Do not lock a configured default before interaction
For a buttons response combining default with allowResponseChange: false, generateInitFields initializes ans.value from the default, so this condition immediately disables every option before the participant has selected anything. The participant can therefore never replace the preselected default; base the lock on the first actual response interaction rather than merely on whether the initialized field has a value.
Useful? React with 👍 / 👎.
|
5ac01f0 to
ca179dc
Compare
JackWilb
left a comment
There was a problem hiding this comment.
Request changes. The main flow needs a few correctness fixes before merge. The inline comments call out the issues affecting training feedback, restored/default answers, response locking, inherited components, navigation timers, and test coverage. Please address those cases and add focused component-level tests for the real user interactions.
| return undefined; | ||
| } | ||
|
|
||
| const hasFeedback = 'provideFeedback' in readyResponse && Boolean(readyResponse.provideFeedback); |
There was a problem hiding this comment.
This check looks for provideFeedback on the response, but training feedback is configured on the component. For a valid training component this is always false, so the timer can advance without Check Answer and skip incorrect-answer attempts. Please gate this path with the same training state used by the Next button.
| handleNextClickRef.current = handleNextClick; | ||
| }, [handleNextClick]); | ||
|
|
||
| const prevCombinedValuesRef = useRef(combinedValues); |
There was a problem hiding this comment.
Comparing the shared Redux values treats initialization as a participant interaction. A default or restored answer can therefore arm this timer without a new button selection, including when the participant returns with Previous. Please pass an explicit selection event from the buttons response and arm the timer only for that event.
| return false; | ||
| } | ||
|
|
||
| if (response.allowResponseChange === false) { |
There was a problem hiding this comment.
Clearing a default sends an empty value through the same interaction path, sets the interaction flag, and then locks every option when allowResponseChange is false. That leaves a required response empty and uneditable. Only lock after a non-empty option is selected, or make Clear Selection unavailable in this mode.
| (response) => response.type === 'buttons' && response.autoAdvanceToNextStep === true, | ||
| ); | ||
| const isAutoAdvance = hasAutoAdvanceButton || resolvedComponent.nextButtonAutoAdvanceTime !== undefined; | ||
| resolvedComponent.nextButtonHidden = resolvedComponent.nextButtonHidden ?? (isAutoAdvance ? true : ((studyConfig.uiConfig as { nextButtonHidden?: boolean })?.nextButtonHidden ?? false)); |
There was a problem hiding this comment.
For an inherited component, studyComponentToIndividualComponent returns a merged clone, so this nextButtonHidden default is discarded. Runtime resolution then falls back to showing Next, contrary to the auto-advance default. Compute the default at the runtime owner or persist it on the actual component entry, and cover inherited/library cases.
| autoAdvanceIdentifierRef.current = identifier; | ||
| if (readyResponse.type === 'buttons') { | ||
| const delay = readyResponse.autoAdvanceDelay ?? 0; | ||
| autoAdvanceTimeoutRef.current = setTimeout(() => { |
There was a problem hiding this comment.
This direct call bypasses NextButton's nextButtonEnableTime and nextButtonDisableTime checks. A response can advance before Next is enabled or after it has timed out. Please share the navigation eligibility check with response auto-advance, or reject/document incompatible timer combinations.
| expect(mockNavigate).toHaveBeenCalledWith('/study-1/1'); | ||
| }); | ||
|
|
||
| test('locks response options immediately during autoAdvanceDelay before navigation occurs', async () => { |
There was a problem hiding this comment.
These tests do not mount ResponseBlock or ResponseSwitcher; they manually invoke goToNextStep and create unrelated timers. They would pass if the new feature were removed. Please add focused tests that render the response UI and exercise real selection, delay, training, Previous/default, cross-location, and clear-selection behavior.
Does this PR close any open issues?
Closes #1447
Give a longer description of what this PR addresses and why it's needed
This PR adds the options autoAdvanceToNextStep, autoAdvanceDelay and allowResponseChange to the ButtonsResponse. Studies sometimes want the user to automatically navigate to the next page/question if some input was given especially when a button is pressed. The options introduced allow to automatically proceed to the next page. This procedure can be delayed and the given answer can be locked so it cannot be changed even if the delay is active and the next question is not shown immediately.