Skip to content

Commit 826c193

Browse files
authored
Merge pull request #894 from rasmi/conditions-refactor
Conditions refactor
2 parents 3948285 + 795a168 commit 826c193

File tree

9 files changed

+1432
-194
lines changed

9 files changed

+1432
-194
lines changed

frontend/src/components/stages/condition_editor.ts

Lines changed: 2 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,15 @@ import {
1616
ComparisonCondition,
1717
ConditionOperator,
1818
ComparisonOperator,
19+
ConditionTarget,
20+
ConditionTargetReference,
1921
createConditionGroup,
2022
createComparisonCondition,
2123
getComparisonOperatorLabel,
22-
ConditionTargetReference,
23-
SurveyQuestion,
24-
SurveyQuestionKind,
25-
MultipleChoiceSurveyQuestion,
2624
} from '@deliberation-lab/utils';
2725

2826
import {styles} from './condition_editor.scss';
2927

30-
export interface ConditionTarget {
31-
ref: ConditionTargetReference; // Structured reference
32-
label: string;
33-
type: 'text' | 'number' | 'boolean' | 'choice';
34-
choices?: Array<{id: string; label: string}>;
35-
stageName?: string; // Optional stage name for display
36-
}
37-
3828
/** Reusable condition editor component */
3929
@customElement('condition-editor')
4030
export class ConditionEditor extends MobxLitElement {
@@ -446,51 +436,3 @@ export class ConditionEditor extends MobxLitElement {
446436
this.onConditionChange(this.condition);
447437
}
448438
}
449-
450-
/** Helper to convert survey questions to condition targets */
451-
export function surveyQuestionsToConditionTargets(
452-
questions: SurveyQuestion[],
453-
stageId: string,
454-
stageName?: string,
455-
): ConditionTarget[] {
456-
return questions.map((q) => {
457-
let type: ConditionTarget['type'] = 'text';
458-
let choices: ConditionTarget['choices'] = undefined;
459-
460-
switch (q.kind) {
461-
case SurveyQuestionKind.TEXT:
462-
type = 'text';
463-
break;
464-
case SurveyQuestionKind.CHECK:
465-
type = 'boolean';
466-
break;
467-
case SurveyQuestionKind.SCALE:
468-
type = 'number';
469-
break;
470-
case SurveyQuestionKind.MULTIPLE_CHOICE:
471-
type = 'choice';
472-
const mcQuestion = q as MultipleChoiceSurveyQuestion;
473-
choices = mcQuestion.options.map((opt) => ({
474-
id: opt.id,
475-
label: opt.text || `Option ${opt.id}`,
476-
}));
477-
break;
478-
}
479-
480-
// Create structured reference
481-
const ref: ConditionTargetReference = {
482-
stageId: stageId,
483-
questionId: q.id,
484-
};
485-
486-
const label = q.questionTitle || `Question ${q.id}`;
487-
488-
return {
489-
ref,
490-
label,
491-
type,
492-
choices,
493-
stageName,
494-
};
495-
});
496-
}

frontend/src/components/stages/survey_editor.ts

Lines changed: 21 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,23 @@ import {CSSResultGroup, html, nothing} from 'lit';
33
import {customElement, property} from 'lit/decorators.js';
44
import '../stages/survey_editor_menu';
55
import '@material/web/checkbox/checkbox.js';
6-
import './condition_editor';
76
import '../../pair-components/textarea_template';
8-
import {
9-
ConditionTarget,
10-
surveyQuestionsToConditionTargets,
11-
} from './condition_editor';
127

138
import {core} from '../../core/core';
149
import {AuthService} from '../../services/auth.service';
1510
import {ExperimentEditor} from '../../services/experiment.editor';
16-
11+
import {renderConditionEditor} from '../../shared/condition_editor.utils';
1712
import {
1813
CheckSurveyQuestion,
1914
Condition,
15+
getConditionTargetsFromStages,
2016
MultipleChoiceItem,
2117
MultipleChoiceSurveyQuestion,
2218
ScaleSurveyQuestion,
2319
SurveyPerParticipantStageConfig,
2420
SurveyStageConfig,
2521
SurveyQuestion,
2622
SurveyQuestionKind,
27-
StageKind,
2823
TextSurveyQuestion,
2924
createMultipleChoiceItem,
3025
} from '@deliberation-lab/utils';
@@ -193,9 +188,11 @@ export class SurveyEditor extends MobxLitElement {
193188
`;
194189
}
195190

196-
private renderConditionEditor(question: SurveyQuestion, index: number) {
191+
private renderQuestionConditionEditor(
192+
question: SurveyQuestion,
193+
index: number,
194+
) {
197195
if (!this.stage) return nothing;
198-
if (!this.authService.showAlphaFeatures) return nothing;
199196

200197
const onConditionChange = (condition: Condition | undefined) => {
201198
this.updateQuestion(
@@ -207,53 +204,20 @@ export class SurveyEditor extends MobxLitElement {
207204
);
208205
};
209206

210-
// Get all questions before this one in current stage (can only reference previous questions)
211-
const currentStageQuestions = this.stage.questions.slice(0, index);
212-
const currentTargets = surveyQuestionsToConditionTargets(
213-
currentStageQuestions,
207+
// Get condition targets from all preceding stages and questions before this one
208+
const targets = getConditionTargetsFromStages(
209+
this.experimentEditor.stages,
214210
this.stage.id,
215-
this.stage.name,
211+
{includeCurrentStage: true, currentStageQuestionIndex: index},
216212
);
217213

218-
// Get questions from all previous stages
219-
const allTargets: ConditionTarget[] = [...currentTargets];
220-
const currentStageIndex = this.experimentEditor.stages.findIndex(
221-
(s) => s.id === this.stage?.id,
222-
);
223-
224-
if (currentStageIndex > 0) {
225-
// Add questions from previous stages
226-
for (let i = 0; i < currentStageIndex; i++) {
227-
const prevStage = this.experimentEditor.stages[i];
228-
if (
229-
prevStage.kind === StageKind.SURVEY ||
230-
prevStage.kind === StageKind.SURVEY_PER_PARTICIPANT
231-
) {
232-
const prevSurveyStage = prevStage as
233-
| SurveyStageConfig
234-
| SurveyPerParticipantStageConfig;
235-
const prevTargets = surveyQuestionsToConditionTargets(
236-
prevSurveyStage.questions,
237-
prevStage.id,
238-
prevStage.name,
239-
);
240-
allTargets.push(...prevTargets);
241-
}
242-
}
243-
}
244-
245-
if (allTargets.length === 0) {
246-
return nothing; // No questions to reference
247-
}
248-
249-
return html`
250-
<condition-editor
251-
.condition=${question.condition}
252-
.targets=${allTargets}
253-
.disabled=${!this.experimentEditor.canEditStages}
254-
.onConditionChange=${onConditionChange}
255-
></condition-editor>
256-
`;
214+
return renderConditionEditor({
215+
condition: question.condition,
216+
targets,
217+
showAlphaFeatures: this.authService.showAlphaFeatures,
218+
canEdit: this.experimentEditor.canEditStages,
219+
onConditionChange,
220+
});
257221
}
258222

259223
private renderQuestionNav(question: SurveyQuestion, index: number) {
@@ -329,7 +293,7 @@ export class SurveyEditor extends MobxLitElement {
329293
>Make this question required for participants</span
330294
>
331295
</label>
332-
${this.renderConditionEditor(question, index)}
296+
${this.renderQuestionConditionEditor(question, index)}
333297
`;
334298
}
335299

@@ -406,7 +370,7 @@ export class SurveyEditor extends MobxLitElement {
406370
>
407371
Add multiple choice item
408372
</pr-button>
409-
${this.renderConditionEditor(question, index)}
373+
${this.renderQuestionConditionEditor(question, index)}
410374
`;
411375
}
412376

@@ -588,7 +552,7 @@ export class SurveyEditor extends MobxLitElement {
588552
Optional: Display the scale as a slider instead of radio buttons
589553
</span>
590554
</label>
591-
${this.renderConditionEditor(question, index)}
555+
${this.renderQuestionConditionEditor(question, index)}
592556
`;
593557
}
594558

@@ -640,7 +604,7 @@ export class SurveyEditor extends MobxLitElement {
640604
/>
641605
</div>
642606
</div>
643-
${this.renderConditionEditor(question, index)}
607+
${this.renderQuestionConditionEditor(question, index)}
644608
`;
645609
}
646610
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {html, nothing} from 'lit';
2+
import {Condition, ConditionTarget} from '@deliberation-lab/utils';
3+
4+
// Ensure condition-editor component is registered
5+
import '../components/stages/condition_editor';
6+
7+
export interface RenderConditionEditorOptions {
8+
condition: Condition | undefined;
9+
targets: ConditionTarget[];
10+
showAlphaFeatures: boolean | undefined;
11+
canEdit: boolean;
12+
onConditionChange: (condition: Condition | undefined) => void;
13+
}
14+
15+
/**
16+
* Render a condition editor with standard checks.
17+
* Returns nothing if alpha features are disabled or there are no valid targets.
18+
*/
19+
export function renderConditionEditor(options: RenderConditionEditorOptions) {
20+
const {condition, targets, showAlphaFeatures, canEdit, onConditionChange} =
21+
options;
22+
23+
if (!showAlphaFeatures) return nothing; // Treats undefined as false
24+
if (targets.length === 0) return nothing;
25+
26+
return html`
27+
<condition-editor
28+
.condition=${condition}
29+
.targets=${targets}
30+
.disabled=${!canEdit}
31+
.onConditionChange=${onConditionChange}
32+
></condition-editor>
33+
`;
34+
}

utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ export * from './stages/transfer_stage.validation';
145145
export * from './utils/algebraic.utils';
146146
export * from './utils/cache.utils';
147147
export * from './utils/condition';
148+
export * from './utils/condition.utils';
148149
export * from './utils/condition.validation';
149150
export * from './utils/object.utils';
150151
export * from './utils/random.utils';

utils/src/stages/survey_stage.ts

Lines changed: 4 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import {generateId} from '../shared';
22
import {
33
Condition,
4-
ConditionTargetReference,
54
evaluateCondition,
65
extractConditionDependencies,
76
extractMultipleConditionDependencies,
87
} from '../utils/condition';
8+
import {getConditionDependencyValuesWithCurrentStage} from '../utils/condition.utils';
99
import {
1010
BaseStageConfig,
1111
BaseStageParticipantAnswer,
@@ -326,7 +326,7 @@ export function getVisibleSurveyQuestions(
326326
);
327327

328328
// Get the actual values for all condition targets
329-
const targetValues = getConditionDependencyValues(
329+
const targetValues = getConditionDependencyValuesWithCurrentStage(
330330
allDependencies,
331331
currentStageId,
332332
currentStageAnswers,
@@ -358,7 +358,7 @@ export function isQuestionVisible(
358358
const dependencies = extractConditionDependencies(question.condition);
359359

360360
// Get the actual values for this question's condition targets
361-
const targetValues = getConditionDependencyValues(
361+
const targetValues = getConditionDependencyValuesWithCurrentStage(
362362
dependencies,
363363
currentStageId,
364364
currentStageAnswers,
@@ -369,65 +369,8 @@ export function isQuestionVisible(
369369
return evaluateCondition(question.condition, targetValues);
370370
}
371371

372-
/** Get the actual answer values for the specified dependencies. */
373-
function getConditionDependencyValues(
374-
dependencies: ConditionTargetReference[],
375-
currentStageId: string,
376-
currentStageAnswers: Record<string, SurveyAnswer>,
377-
allStageAnswers?: Record<string, StageParticipantAnswer>,
378-
targetParticipantId?: string, // For survey-per-participant: which participant is being evaluated
379-
): Record<string, unknown> {
380-
const values: Record<string, unknown> = {};
381-
382-
for (const targetRef of dependencies) {
383-
// Build the key for this dependency
384-
// Using :: as separator since it's unlikely to appear in IDs
385-
const dataKey = `${targetRef.stageId}::${targetRef.questionId}`;
386-
387-
if (targetRef.stageId === currentStageId) {
388-
// Reference to current stage
389-
const answer = currentStageAnswers[targetRef.questionId];
390-
if (answer) {
391-
values[dataKey] = extractAnswerValue(answer);
392-
}
393-
} else if (allStageAnswers && allStageAnswers[targetRef.stageId]) {
394-
// Reference to another stage
395-
const stageAnswer = allStageAnswers[targetRef.stageId];
396-
if (stageAnswer && 'answerMap' in stageAnswer) {
397-
if (stageAnswer.kind === StageKind.SURVEY) {
398-
// Regular survey stage
399-
const surveyAnswer = stageAnswer as SurveyStageParticipantAnswer;
400-
const answer = surveyAnswer.answerMap[targetRef.questionId];
401-
if (answer) {
402-
values[dataKey] = extractAnswerValue(answer);
403-
}
404-
} else if (stageAnswer.kind === StageKind.SURVEY_PER_PARTICIPANT) {
405-
// Survey-per-participant: get answer about the target participant
406-
const surveyAnswer =
407-
stageAnswer as SurveyPerParticipantStageParticipantAnswer;
408-
409-
// For cross-stage references, we need targetParticipantId to know which answer to use
410-
if (
411-
targetParticipantId &&
412-
surveyAnswer.answerMap[targetRef.questionId]
413-
) {
414-
const participantAnswers =
415-
surveyAnswer.answerMap[targetRef.questionId];
416-
if (participantAnswers && participantAnswers[targetParticipantId]) {
417-
const answer = participantAnswers[targetParticipantId];
418-
values[dataKey] = extractAnswerValue(answer);
419-
}
420-
}
421-
}
422-
}
423-
}
424-
}
425-
426-
return values;
427-
}
428-
429372
/** Extract the value from a survey answer */
430-
function extractAnswerValue(
373+
export function extractAnswerValue(
431374
answer: SurveyAnswer,
432375
): string | number | boolean | undefined {
433376
switch (answer.kind) {

0 commit comments

Comments
 (0)