Skip to content

Commit 8ed3253

Browse files
authored
feat: fix multistep questions (#799)
# Add `onStepChange` callback to ActionSelector component ### TL;DR Added a new `onStepChange` callback to the ActionSelector component to notify parent components when steps change. closes #795 ### What changed? - Added `onStepChange` callback prop to the ActionSelector component - Implemented a new `setStep` function in `useActionSelectorState` that calls the callback when steps change - Updated all step navigation logic to use this new function - Fixed QuestionPermission to sync its state with ActionSelector step changes - Added a regression test story for multi-step questions ### How to test? 1. Run the Storybook and navigate to the new "QuestionMultiStepSync" story 2. Verify that advancing through steps updates the question text and options correctly 3. Check that step navigation works in both directions 4. Confirm the "stepChanged" action is logged in the Actions panel when steps change ### Why make this change? This change fixes a synchronization issue between the ActionSelector component and its parent components. Previously, when steps changed internally in the ActionSelector, parent components weren't always notified, leading to UI inconsistencies. The new callback ensures parent components can stay in sync with the current step, properly updating question text and options.
1 parent 6f7c095 commit 8ed3253

6 files changed

Lines changed: 51 additions & 8 deletions

File tree

apps/twig/src/renderer/components/ActionSelector.stories.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const meta: Meta<typeof ActionSelector> = {
1212
onMultiSelect: { action: "multiSelected" },
1313
onCancel: { action: "cancelled" },
1414
onStepAnswer: { action: "stepAnswered" },
15+
onStepChange: { action: "stepChanged" },
1516
},
1617
};
1718

apps/twig/src/renderer/components/action-selector/ActionSelector.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export function ActionSelector({
2525
onSelect,
2626
onMultiSelect,
2727
onCancel,
28+
onStepChange,
2829
onStepAnswer,
2930
}: ActionSelectorProps) {
3031
const state = useActionSelectorState({
@@ -37,6 +38,7 @@ export function ActionSelector({
3738
initialSelections,
3839
onSelect,
3940
onMultiSelect,
41+
onStepChange,
4042
onStepAnswer,
4143
});
4244

apps/twig/src/renderer/components/action-selector/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export interface ActionSelectorProps {
3232
onSelect: (optionId: string, customInput?: string) => void;
3333
onMultiSelect?: (optionIds: string[], customInput?: string) => void;
3434
onCancel?: () => void;
35+
onStepChange?: (stepIndex: number) => void;
3536
onStepAnswer?: (
3637
stepIndex: number,
3738
optionIds: string[],

apps/twig/src/renderer/components/action-selector/useActionSelectorState.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ interface UseActionSelectorStateProps {
2222
initialSelections?: string[];
2323
onSelect: ActionSelectorProps["onSelect"];
2424
onMultiSelect: ActionSelectorProps["onMultiSelect"];
25+
onStepChange: ActionSelectorProps["onStepChange"];
2526
onStepAnswer: ActionSelectorProps["onStepAnswer"];
2627
}
2728

@@ -35,6 +36,7 @@ export function useActionSelectorState({
3536
initialSelections,
3637
onSelect,
3738
onMultiSelect,
39+
onStepChange,
3840
onStepAnswer,
3941
}: UseActionSelectorStateProps) {
4042
const [selectedIndex, setSelectedIndex] = useState(0);
@@ -80,6 +82,15 @@ export function useActionSelectorState({
8082
setInternalStep(currentStep);
8183
}, [currentStep]);
8284

85+
const setStep = useCallback(
86+
(nextStep: number) => {
87+
if (nextStep === activeStep) return;
88+
onStepChange?.(nextStep);
89+
setInternalStep(nextStep);
90+
},
91+
[activeStep, onStepChange],
92+
);
93+
8394
const saveCurrentStepAnswer = useCallback(() => {
8495
const checkedIds = Array.from(checkedOptions);
8596
const answer: StepAnswer = {
@@ -138,15 +149,15 @@ export function useActionSelectorState({
138149
if (!hasSteps) return;
139150
saveCurrentStepAnswer();
140151
const prevStep = activeStep > 0 ? activeStep - 1 : numSteps - 1;
141-
setInternalStep(prevStep);
142-
}, [hasSteps, activeStep, numSteps, saveCurrentStepAnswer]);
152+
setStep(prevStep);
153+
}, [hasSteps, activeStep, numSteps, saveCurrentStepAnswer, setStep]);
143154

144155
const moveToNextStep = useCallback(() => {
145156
if (!hasSteps) return;
146157
saveCurrentStepAnswer();
147158
const nextStep = activeStep < numSteps - 1 ? activeStep + 1 : 0;
148-
setInternalStep(nextStep);
149-
}, [hasSteps, activeStep, numSteps, saveCurrentStepAnswer]);
159+
setStep(nextStep);
160+
}, [hasSteps, activeStep, numSteps, saveCurrentStepAnswer, setStep]);
150161

151162
const toggleCheck = useCallback(
152163
(optionId: string) => {
@@ -194,9 +205,13 @@ export function useActionSelectorState({
194205
const selected = allOptions[selectedIndex];
195206

196207
if (isSubmitOption(selected.id)) {
208+
if (!showSubmitButton) {
209+
onSelect(selected.id);
210+
return;
211+
}
197212
if (hasSteps && activeStep < numSteps - 1) {
198213
saveCurrentStepAnswer();
199-
setInternalStep(activeStep + 1);
214+
setStep(activeStep + 1);
200215
} else {
201216
if (multiSelect) {
202217
handleSubmitMulti();
@@ -239,9 +254,13 @@ export function useActionSelectorState({
239254
setSelectedIndex(index);
240255

241256
if (isSubmitOption(selected.id)) {
257+
if (!showSubmitButton) {
258+
onSelect(selected.id);
259+
return;
260+
}
242261
if (hasSteps && activeStep < numSteps - 1) {
243262
saveCurrentStepAnswer();
244-
setInternalStep(activeStep + 1);
263+
setStep(activeStep + 1);
245264
} else {
246265
if (multiSelect) {
247266
handleSubmitMulti();
@@ -278,9 +297,9 @@ export function useActionSelectorState({
278297
const handleStepClick = useCallback(
279298
(stepIndex: number) => {
280299
saveCurrentStepAnswer();
281-
setInternalStep(stepIndex);
300+
setStep(stepIndex);
282301
},
283-
[saveCurrentStepAnswer],
302+
[saveCurrentStepAnswer, setStep],
284303
);
285304

286305
const handleEscape = useCallback(() => {

apps/twig/src/renderer/components/permissions/PermissionSelector.stories.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,21 @@ export const QuestionMultiStep: Story = {
718718
},
719719
};
720720

721+
export const QuestionMultiStepSync: Story = {
722+
args: {
723+
toolCall: buildQuestionToolCallData(multiStepQuestions),
724+
options: buildQuestionOptions(multiStepQuestions[0]),
725+
},
726+
parameters: {
727+
docs: {
728+
description: {
729+
story:
730+
"Regression: advancing steps should update the question text and options for each step.",
731+
},
732+
},
733+
},
734+
};
735+
721736
const multiSelectQuestion: QuestionItem[] = [
722737
{
723738
question: "Which features do you want to enable?",

apps/twig/src/renderer/components/permissions/QuestionPermission.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,10 @@ export function QuestionPermission({
200200
[],
201201
);
202202

203+
const handleStepChange = useCallback((stepIndex: number) => {
204+
setActiveStep(stepIndex);
205+
}, []);
206+
203207
const hasUnanswered = useMemo(() => {
204208
for (let i = 0; i < totalQuestions; i++) {
205209
if (!isQuestionAnswered(stepAnswers, i)) {
@@ -275,6 +279,7 @@ export function QuestionPermission({
275279
onSelect={handleSelect}
276280
onMultiSelect={handleMultiSelect}
277281
onCancel={onCancel}
282+
onStepChange={handleStepChange}
278283
onStepAnswer={handleStepAnswer}
279284
/>
280285
);

0 commit comments

Comments
 (0)