diff --git a/frontend/src/components/stages/survey_editor.ts b/frontend/src/components/stages/survey_editor.ts index 122b83c15..07512e743 100644 --- a/frontend/src/components/stages/survey_editor.ts +++ b/frontend/src/components/stages/survey_editor.ts @@ -15,6 +15,7 @@ import { getConditionTargetsFromStages, MultipleChoiceItem, MultipleChoiceSurveyQuestion, + sanitizeSurveyQuestionConditions, ScaleSurveyQuestion, SurveyPerParticipantStageConfig, SurveyStageConfig, @@ -103,20 +104,34 @@ export class SurveyEditor extends MobxLitElement { } } + /** + * Update the stage with new questions, automatically sanitizing conditions. + */ + private updateStageQuestions(questions: SurveyQuestion[]) { + if (!this.stage) return; + + const sanitizedQuestions = sanitizeSurveyQuestionConditions( + questions, + this.stage.id, + ); + + this.experimentEditor.updateStage({ + ...this.stage, + questions: sanitizedQuestions, + }); + } + moveQuestionUp(index: number) { if (!this.stage) return; const questions = [ ...this.stage.questions.slice(0, index - 1), - ...this.stage.questions.slice(index, index + 1), - ...this.stage.questions.slice(index - 1, index), + this.stage.questions[index], + this.stage.questions[index - 1], ...this.stage.questions.slice(index + 1), ]; - this.experimentEditor.updateStage({ - ...this.stage, - questions, - }); + this.updateStageQuestions(questions); } moveQuestionDown(index: number) { @@ -124,15 +139,12 @@ export class SurveyEditor extends MobxLitElement { const questions = [ ...this.stage.questions.slice(0, index), - ...this.stage.questions.slice(index + 1, index + 2), - ...this.stage.questions.slice(index, index + 1), + this.stage.questions[index + 1], + this.stage.questions[index], ...this.stage.questions.slice(index + 2), ]; - this.experimentEditor.updateStage({ - ...this.stage, - questions, - }); + this.updateStageQuestions(questions); } deleteQuestion(index: number) { @@ -143,10 +155,7 @@ export class SurveyEditor extends MobxLitElement { ...this.stage.questions.slice(index + 1), ]; - this.experimentEditor.updateStage({ - ...this.stage, - questions, - }); + this.updateStageQuestions(questions); } updateQuestion(question: SurveyQuestion, index: number) { diff --git a/utils/src/utils/condition.utils.ts b/utils/src/utils/condition.utils.ts index b9e201b71..dd976dfcd 100644 --- a/utils/src/utils/condition.utils.ts +++ b/utils/src/utils/condition.utils.ts @@ -10,6 +10,7 @@ import { ConditionTargetReference, getConditionTargetKey, extractMultipleConditionDependencies, + extractConditionDependencies, evaluateCondition, Condition, } from './condition'; @@ -311,3 +312,50 @@ export function getConditionTargetsFromStages( return targets; } + +// ============================================================================ +// Condition Sanitization Utilities +// ============================================================================ + +/** + * Sanitize survey question conditions to ensure they only reference valid targets. + * + * A condition is invalid if it references: + * - A question that doesn't exist in the list + * - A question that comes at or after the current question's position + * + * Invalid conditions are cleared (set to undefined) to prevent rendering issues. + * + * @param questions - The list of survey questions to sanitize + * @param stageId - The ID of the stage containing these questions + * @returns A new array with invalid conditions cleared + */ +export function sanitizeSurveyQuestionConditions< + T extends {id: string; condition?: Condition}, +>(questions: T[], stageId: string): T[] { + // Build a map of question ID to its index in the ordering + const questionIndexMap = new Map(); + questions.forEach((q, idx) => { + questionIndexMap.set(q.id, idx); + }); + + return questions.map((question, index) => { + if (!question.condition) return question; + + const dependencies = extractConditionDependencies(question.condition); + + // Check if any dependency in the same stage is invalid + const hasInvalidDependency = dependencies.some((dep) => { + if (dep.stageId !== stageId) return false; // Other stages are fine + + const refIndex = questionIndexMap.get(dep.questionId); + // Invalid if: question doesn't exist, or comes at/after current position + return refIndex === undefined || refIndex >= index; + }); + + if (hasInvalidDependency) { + return {...question, condition: undefined}; + } + return question; + }); +}