Skip to content

Instantly share code, notes, and snippets.

@dumitruPuggle
Created March 26, 2024 16:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dumitruPuggle/5e7c58bde3d35643a2089f1bf3d0b3d2 to your computer and use it in GitHub Desktop.
Save dumitruPuggle/5e7c58bde3d35643a2089f1bf3d0b3d2 to your computer and use it in GitHub Desktop.
import {
MappedFormWithFullScores,
MappedScoresToChoices,
MappedScoresToOpinionScale,
MappedScoresToYesNo,
TypeformField,
TypeformForm,
} from "../../../../shared/typeform";
export class TypeformFormDefinition {
constructor(public typeformForm: TypeformForm) {}
public get logicFields(): TypeformForm["logic"] {
return this.typeformForm.logic.map((logicField) => logicField).filter((logicField) => logicField.type === "field");
}
public get fields(): TypeformField<undefined>[] {
return this.typeformForm.fields;
}
public findFieldById(
fieldId: string,
fieldsStartPoint: TypeformField<any>[] = this.fields
): TypeformField<undefined> {
const findResult = fieldsStartPoint.find((field) => field.id === fieldId);
if (!findResult) {
console.log("findResult", findResult);
throw new Error(`Field with ID ${fieldId} not found`);
}
return findResult;
}
public findFieldByRef(
fieldRef: string,
fieldsStartPoint: TypeformField<any>[] = this.fields
): TypeformField<undefined> {
const findResult = fieldsStartPoint.find((field) => field.ref === fieldRef);
if (!findResult) {
throw new Error(`Field with ref ${fieldRef} not found`);
}
return findResult;
}
/**
* Returns all the fields from logic object that contain a logic element that adds a score,
* based on a condition.
*
* In two words, this is the place where we have all the scores, in a very stupid format
* provided by Typeform.
*/
public get logicFieldsWithAddScoreAction(): TypeformForm["logic"] {
return this.logicFields.filter((logicField) =>
logicField.actions.find(
(logicFieldAction) =>
logicFieldAction.details.target?.value === "score" &&
logicFieldAction.details.value?.type === "constant" &&
typeof logicFieldAction.details.value?.value === "number"
)
);
}
/**
*
*/
public mapScoresForYesNoChoices(
yesNoFieldId: string,
fieldsStartPoint: TypeformField<any>[] = this.fields
): MappedScoresToYesNo {
const yesNoField = this.findFieldById(yesNoFieldId, fieldsStartPoint);
if (yesNoField.type !== "yes_no") {
throw new Error(`Field with ID ${yesNoFieldId} is not a yes/no field`);
}
let scores: { choiceBooleanName: boolean; score: number }[] = [];
this.logicFieldsWithAddScoreAction.forEach((logicField) => {
logicField.actions.forEach((action) => {
const constantScoreChoiceQuery = action.condition.vars.find(
(variable) => variable.type === "constant" && typeof variable.value === "boolean"
);
if (constantScoreChoiceQuery) {
const scoreValue = action.details.value?.value;
if (typeof scoreValue === "number") {
scores.push({
choiceBooleanName: action.condition.vars.find((variable) => variable.type === "constant")
?.value as boolean,
score: scoreValue,
});
}
}
});
});
const mappedScoresToChoices: MappedScoresToYesNo = {
...yesNoField,
properties: {
...yesNoField.properties,
__yesNoMappedScores: scores,
},
};
return mappedScoresToChoices;
}
/**
* Finds the scores for the specified `dropdown` or `multiple_choice` like field type inside the logic definition
* of the form, and then maps the found scores back to field.
*/
public mapScoresForDropdownFieldChoices(
dropdownFieldId: string,
fieldsStartPoint: TypeformField<any>[] = this.fields
): MappedScoresToChoices {
const dropdownField = this.findFieldById(dropdownFieldId, fieldsStartPoint);
if (dropdownField.type !== "dropdown" && dropdownField.type !== "multiple_choice") {
throw new Error(`Field with ID ${dropdownFieldId} is not a dropdown or multiple choice field`);
}
const choicesWithScores = dropdownField.properties?.choices?.map((choice) => {
let score = undefined;
this.logicFieldsWithAddScoreAction.forEach((logicField) => {
logicField.actions.forEach((action) => {
if (action.condition.vars.some((variable) => variable.type === "choice" && variable.value === choice.ref)) {
const potentialScore = action.details.value?.value;
if (typeof potentialScore === "number") {
score = { score: potentialScore };
}
}
});
});
if (score !== undefined) {
return { ...choice, __mappedScores: score };
} else {
return choice;
}
});
return {
...dropdownField,
properties: {
...dropdownField.properties,
choices: choicesWithScores,
},
};
}
/**
* Finds the scores for the specified `dropdown` or `multiple_choice` like field type inside the logic definition
* of the form, and then maps the found scores back to field.
*/
public mapScoresForOpinionScaleOptions(
opinionScaleFieldId: string,
fieldsStartPoint: TypeformField<any>[] = this.fields
): MappedScoresToOpinionScale {
const opinionScaleField = this.findFieldById(opinionScaleFieldId, fieldsStartPoint);
if (opinionScaleField.type !== "opinion_scale") {
throw new Error(`Field with ID ${opinionScaleFieldId} is not an opinion scale field`);
}
let scores: { step: number; score: number }[] = [];
this.logicFieldsWithAddScoreAction.forEach((logicField) => {
logicField.actions.forEach((action) => {
const constantScoreChoiceQuery = action.condition.vars.find(
(variable) => variable.type === "constant" && typeof variable.value === "number"
);
if (
action.condition.vars.some(
(variable) => variable.type === "field" && variable.value === opinionScaleField.ref
) &&
constantScoreChoiceQuery
) {
const scoreValue = action.details.value?.value;
if (typeof scoreValue === "number") {
scores.push({
step: constantScoreChoiceQuery.value as number,
score: scoreValue,
});
}
}
});
});
scores.sort((a, b) => a.step - b.step);
const mappedScoresToOpinionScale: MappedScoresToOpinionScale = {
...opinionScaleField,
properties: {
...opinionScaleField.properties,
__opinionScaleMappedScore: scores,
},
};
return mappedScoresToOpinionScale;
}
private processedGroups: Set<string> = new Set();
public mapScoresForGroupField(groupFieldId: string): MappedFormWithFullScores {
if (this.processedGroups.has(groupFieldId)) {
// This group has already been processed, stop the recursion:
return [];
}
this.processedGroups.add(groupFieldId);
const groupField = this.findFieldById(groupFieldId);
if (groupField.type !== "group") {
throw new Error(`Field with ID ${groupFieldId} is not a group field`);
}
return this.mapAllScoresToSupportedFields(groupField.properties.fields);
}
public mapAllScoresToSupportedFields(
defaultFieldsLocation: TypeformField<any>[] = this.fields
): MappedFormWithFullScores {
return defaultFieldsLocation.map((field) => {
if (field.type === "yes_no") {
return this.mapScoresForYesNoChoices(field.id, defaultFieldsLocation);
} else if (field.type === "dropdown" || field.type === "multiple_choice") {
return this.mapScoresForDropdownFieldChoices(field.id, defaultFieldsLocation);
} else if (field.type === "opinion_scale") {
return this.mapScoresForOpinionScaleOptions(field.id, defaultFieldsLocation);
} else if (field.type === "group") {
return this.mapScoresForGroupField(field.id);
} else if (field.type === "contact_info") {
return field;
}
throw new Error(`Field type: '${field.type}' is not supported`);
}) as MappedFormWithFullScores;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment