Skip to content

Td 2435 generic assessment UI #357

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "traitify-widgets",
"version": "3.7.1",
"version": "3.8.0",
"description": "Traitiy Widgets",
"repository": {
"type": "git",
Expand Down
51 changes: 50 additions & 1 deletion public/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ function createAssessment() {
if(cache.get("surveyType") === "benchmark") { return createWidget(); }
if(cache.get("surveyType") === "cognitive") { return createCognitiveAssessment(); }
if(cache.get("surveyType") === "order") { return createWidget(); }
if(cache.get("surveyType") === "generic") { return createGenericAssessment(); }

const params = {
deck_id: cache.get("deckID"),
Expand Down Expand Up @@ -240,6 +241,25 @@ function createElement(options = {}) {
return element;
}

function createGenericAssessment() {
Traitify.http.authKey = "admin-secret";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also auth key

const query = Traitify.GraphQL.generic.create;
const variables = {
surveyID: cache.get("surveyID"),
profileID: cache.get("profileID")
};

Traitify.http.post(Traitify.GraphQL.generic.path, {query, variables}).then((response) => {
try {
const id = response.data.getOrCreateGenericAssessment.id;
cache.set("assessmentID", id);
} catch(error) {
console.log(error);
}
setTimeout(createWidget, 500);
});
}

function destroyWidget() { Traitify.destroy(); }

function setupTargets() {
Expand Down Expand Up @@ -369,6 +389,7 @@ function setupDom() {
options: [
{text: "Benchmark", value: "benchmark"},
{text: "Cognitive", value: "cognitive"},
{text: "Generic", value: "generic"},
{text: "Order", value: "order"},
{text: "Personality", value: "personality"}
],
Expand Down Expand Up @@ -404,6 +425,9 @@ function setupDom() {
row.appendChild(createOption({name: "orderID", text: "Order ID:"}));
group.appendChild(row)

row = createElement({className: surveyType !== "generic" ? "hide" : "", id: "generic-options"});
row.appendChild(createOption({name: "profileID", text: "Profile ID:"}));
group.appendChild(row);
row = createElement({className: "row"});
row.appendChild(createElement({onClick: createAssessment, tag: "button", text: "Create / Load"}));
group.appendChild(row);
Expand All @@ -427,6 +451,30 @@ function setupCognitive() {
});
}

function setupGeneric() {
Traitify.http.authKey = "admin-secret";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does generic require a secret key to list them?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, secret key is applied to listing query and mutation graphql request

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'd need to change that to public key since this is on the front-end, we can't use a secret key in the candidate's browser

const query = Traitify.GraphQL.generic.surveys;
const variables = {localeKey: cache.get("locale")};

Traitify.http.post(Traitify.GraphQL.generic.path, {query, variables}).then((response) => {
try {
const options = response.data.genericSurveys
.map(({id, name}) => ({text: name, value: id}))
.sort((a, b) => a.text.localeCompare(b.text));

document.querySelector("#generic-options").appendChild(createOption({
name: "surveyID",
onChange: onInputChange,
options,
text: "Survey:"
}));
} catch(error) {
console.log(error);
}
});

}

function setupTraitify() {
const environment = cache.get("environment");

Expand Down Expand Up @@ -455,7 +503,7 @@ function onSurveyTypeChange(e) {
const name = e.target.name;
const value = e.target.value;
const assessmentID = cache.get(`${value}AssessmentID`);
const otherValues = ["benchmark", "cognitive", "order", "personality"].filter((type) => type !== value);
const otherValues = ["benchmark", "cognitive", "order", "personality", "generic"].filter((type) => type !== value);

cache.set("assessmentID", assessmentID);

Expand All @@ -468,4 +516,5 @@ function onSurveyTypeChange(e) {
setupTraitify();
setupDom();
setupCognitive();
setupGeneric();
createWidget();
6 changes: 4 additions & 2 deletions src/components/common/modal/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import Icon from "components/common/icon";
import useTranslate from "lib/hooks/use-translate";
import style from "./style.scss";

export default function Modal({children, onClose, title}) {
export default function Modal({children, containerClass = null, onClose, title}) {
const translate = useTranslate();
const sectionClass = [style.modalContainer, containerClass].filter(Boolean).join(" ");
return (
<div className={`${style.modal} ${style.container}`}>
<section className={style.modalContainer}>
<section className={sectionClass}>
<div className={style.modalContent}>
<div className={style.header}>
<div>{title}</div>
Expand All @@ -34,6 +35,7 @@ export default function Modal({children, onClose, title}) {

Modal.propTypes = {
children: PropTypes.node.isRequired,
containerClass: PropTypes.string,
onClose: PropTypes.func.isRequired,
title: PropTypes.string.isRequired
};
2 changes: 2 additions & 0 deletions src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import RecommendationChart from "./results/recommendation/chart";
import Status from "./status";
import Survey from "./survey";
import CognitiveSurvey from "./survey/cognitive";
import Generic from "./survey/generic";
import PersonalitySurvey from "./survey/personality";

export default {
Expand Down Expand Up @@ -86,6 +87,7 @@ export default {
Survey: {
Cognitive: CognitiveSurvey,
Container: Survey,
Generic,
Personality: PersonalitySurvey
}
};
12 changes: 12 additions & 0 deletions src/components/results/generic/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import useResults from "lib/hooks/use-results";

export default function Generic() {
const results = useResults({surveyType: "generic"});
console.log("Generic assessment result:", results);
return (
<div>
<h1>Generic Report</h1>
<p>This is a placeholder for a generic report. Please customize it as needed.</p>
</div>
);
}
20 changes: 20 additions & 0 deletions src/components/survey/generic/container.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import PropTypes from "prop-types";
import style from "./style.scss";

export default function Container({children, progress}) {
return (
<div className={`${style.container}`}>
{progress < 100 && (
<div className={style.progressBar}>
<div className={style.progress} style={{width: `${progress}%`}} />
</div>
)}
{children}
</div>
);
}

Container.propTypes = {
children: PropTypes.node.isRequired,
progress: PropTypes.number.isRequired
};
132 changes: 132 additions & 0 deletions src/components/survey/generic/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import {faArrowLeft} from "@fortawesome/free-solid-svg-icons";
import {useEffect, useState} from "react";
import Icon from "components/common/icon";
import Markdown from "components/common/markdown";
import Modal from "components/common/modal";
import useAssessment from "lib/hooks/use-assessment";
import useGraphql from "lib/hooks/use-graphql";
import useHttp from "lib/hooks/use-http";
import useDidUpdate from "lib/hooks/use-did-update";
import useTranslate from "lib/hooks/use-translate";
import Container from "./container";
import QuestionSet from "./question-set";
import style from "./style.scss";

export default function Generic() {
const [questionSetIndex, setQuestionSetIndex] = useState(0);
const [answers, setAnswers] = useState([]);
const [showInstructions, setShowInstructions] = useState(false);
const [showConclusions, setShowConclusions] = useState(false);
const [submitAttempts, setSubmitAttempts] = useState(0);

const assessment = useAssessment({surveyType: "generic"});
const questionSets = assessment ? assessment.survey.questionSets : [];
const questionCount = questionSets.reduce((count, set) => count + set.questions.length, 0);
const currentQuestionSet = questionSets ? questionSets[questionSetIndex] : {};
const progress = questionSetIndex >= 0 ? (questionSetIndex / questionSets.length) * 100 : 0;
const finished = questionSets.length > 0 && questionCount === answers.length;

const graphQL = useGraphql();
const http = useHttp();
const translate = useTranslate();
const props = {progress};

const updateAnswer = (questionId, selectedOptionId) => {
const currentAnswers = answers.filter((answer) => answer.questionId !== questionId);
setAnswers([...currentAnswers,
{questionId, selectedResponseOptionId: selectedOptionId}]);
};

const next = () => { setQuestionSetIndex(questionSetIndex + 1); };
const back = () => { setQuestionSetIndex(questionSetIndex - 1); };

const onSubmit = () => {
if(submitAttempts > 3) { return; }
const query = graphQL.generic.update;
const variables = {
assessmentID: assessment.id,
answers
};

http.post(graphQL.generic.path, {query, variables}).then(({data, errors}) => {
if(!errors && data.submitGenericAssessmentAnswers) {
setShowConclusions(true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the conclusions might need to be in the results component from a candidate's perspective, so here we'd want either cache the results if they come back from this request, or clear the cache (like in the personality survey) so it automatically requests it again and the results component is rendered.

This part could go fine for now if you want to focus on results in the next PR

} else {
console.warn(errors || data); // eslint-disable-line no-console

setTimeout(() => setSubmitAttempts((x) => x + 1), 2000);
}
});
};

useDidUpdate(() => { onSubmit(); }, [submitAttempts]);

useEffect(() => {
setShowInstructions(true);
}, [assessment]);

useEffect(() => {
if(!finished) { return; }
onSubmit();
}, [finished]);

if(showConclusions) {
return (
<Container {...props}>
<Markdown className={style.markdown}>
{assessment.survey.conclusions}
</Markdown>
<button type="button" className={style.btnPrimary}>Finished!</button>
</Container>
);
}

return (
<Container {...props}>
{currentQuestionSet && (
<QuestionSet
key={questionSetIndex}
questionSet={currentQuestionSet}
updateAnswer={updateAnswer}
next={next}
/>
)}

{questionSetIndex > 0 && (
<button onClick={back} type="button" className={style.back}>
<Icon className={style.icon} alt={translate("back")} icon={faArrowLeft} />
Go Back
</button>
)}
{showInstructions
&& (
<Modal
title="Instructions"
onClose={() => setShowInstructions(false)}
containerClass={style.modalContainer}
>
<Markdown>
{assessment.survey.instructions}
</Markdown>
<hr className={style.grayDivider} />
<div className={style.footer}>
<button
type="button"
className={style.cancelBtn}
onClick={() => setShowInstructions(false)}
>
Cancel
</button>
<button
type="button"
className={style.btnPrimary}
onClick={() => setShowInstructions(false)}
>
{assessment.survey.instructionButton}
</button>
</div>
</Modal>
)}
</Container>
);
}
48 changes: 48 additions & 0 deletions src/components/survey/generic/question-set.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import PropTypes from "prop-types";
import {useEffect, useState} from "react";
import Responses from "./responses";
import style from "./style.scss";

export default function QuestionSet({next, questionSet, updateAnswer}) {
const questionSetClass = [style.questionSet].join(" ");
const [selectedOptions, setSelectedOptions] = useState([]);
const setFinished = questionSet.questions.length === selectedOptions.length;
const selectOption = (questionId, optionId) => {
if(!selectedOptions.includes(questionId)) setSelectedOptions([...selectedOptions, questionId]);
updateAnswer(questionId, optionId);
};

useEffect(() => {
if(!setFinished) return;
next();
}, [setFinished]);

return (
<div className={questionSetClass}>
<img src={questionSet.setImage} alt={questionSet.text} />
<hr />
{questionSet.questions.map((question) => (
<div key={question.id}>
<h3 className={style.question}>{question.text}</h3>
<Responses
responseOptions={question.responseOptions}
updateAnswer={(optionId) => selectOption(question.id, optionId)}
/>
</div>
))}
</div>
);
}

QuestionSet.propTypes = {
next: PropTypes.func.isRequired,
questionSet: PropTypes.shape({
text: PropTypes.string.isRequired,
questions: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
text: PropTypes.string.isRequired
})).isRequired,
setImage: PropTypes.string.isRequired
}).isRequired,
updateAnswer: PropTypes.func.isRequired
};
Loading