Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 25 additions & 16 deletions frontend/src/components/stages/survey_editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
getConditionTargetsFromStages,
MultipleChoiceItem,
MultipleChoiceSurveyQuestion,
sanitizeSurveyQuestionConditions,
ScaleSurveyQuestion,
SurveyPerParticipantStageConfig,
SurveyStageConfig,
Expand Down Expand Up @@ -103,36 +104,47 @@ 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) {
if (!this.stage) return;

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) {
Expand All @@ -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) {
Expand Down
48 changes: 48 additions & 0 deletions utils/src/utils/condition.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
ConditionTargetReference,
getConditionTargetKey,
extractMultipleConditionDependencies,
extractConditionDependencies,
evaluateCondition,
Condition,
} from './condition';
Expand Down Expand Up @@ -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<string, number>();
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;
});
}