Created
March 26, 2024 16:37
-
-
Save dumitruPuggle/5e7c58bde3d35643a2089f1bf3d0b3d2 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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