merge develop into my branch
This commit is contained in:
parent
d0709047cc
commit
1afdec7138
24 changed files with 770 additions and 572 deletions
|
|
@ -15,6 +15,7 @@ module.exports = {
|
||||||
"!src/layouts/part-questions/**/*.vue",
|
"!src/layouts/part-questions/**/*.vue",
|
||||||
"!src/layouts/reveal/**/*.vue",
|
"!src/layouts/reveal/**/*.vue",
|
||||||
"!src/ux-components/text-link/**/*.vue",
|
"!src/ux-components/text-link/**/*.vue",
|
||||||
|
"!src/common-components/question-chain/**/*.vue",
|
||||||
// END
|
// END
|
||||||
], // ! means exclude from coverage.
|
], // ! means exclude from coverage.
|
||||||
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
|
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ describe("buttonQuestion.vue", () => {
|
||||||
const wrapper = shallowMount(buttonQuestion, {
|
const wrapper = shallowMount(buttonQuestion, {
|
||||||
propsData: {
|
propsData: {
|
||||||
isOverflowScrollable: true,
|
isOverflowScrollable: true,
|
||||||
|
groupName: "group-name"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -25,6 +26,7 @@ describe("buttonQuestion.vue", () => {
|
||||||
const wrapper = shallowMount(buttonQuestion, {
|
const wrapper = shallowMount(buttonQuestion, {
|
||||||
propsData: {
|
propsData: {
|
||||||
buttonType: "listCard",
|
buttonType: "listCard",
|
||||||
|
groupName: "group-name"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -39,6 +41,7 @@ describe("buttonQuestion.vue", () => {
|
||||||
const wrapper = shallowMount(buttonQuestion, {
|
const wrapper = shallowMount(buttonQuestion, {
|
||||||
propsData: {
|
propsData: {
|
||||||
buttonType: "listButtonHorizontal",
|
buttonType: "listButtonHorizontal",
|
||||||
|
groupName: "group-name"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -53,6 +56,7 @@ describe("buttonQuestion.vue", () => {
|
||||||
const wrapper = shallowMount(buttonQuestion, {
|
const wrapper = shallowMount(buttonQuestion, {
|
||||||
propsData: {
|
propsData: {
|
||||||
buttonType: "radio",
|
buttonType: "radio",
|
||||||
|
groupName: "group-name"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -77,7 +81,8 @@ describe("buttonQuestion.vue", () => {
|
||||||
// Act
|
// Act
|
||||||
const localThis = {
|
const localThis = {
|
||||||
isWide: false,
|
isWide: false,
|
||||||
answers: ['a', 'b']
|
answers: ['a', 'b'],
|
||||||
|
groupName: "group-name"
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(buttonQuestion.computed.getColLength.call(localThis)).toBe("");
|
expect(buttonQuestion.computed.getColLength.call(localThis)).toBe("");
|
||||||
|
|
@ -109,7 +114,7 @@ describe("buttonQuestion.vue", () => {
|
||||||
describe("buttonQuestion.vue", () => {
|
describe("buttonQuestion.vue", () => {
|
||||||
it("Should trigger event modelValue change to new value on when radio button selected", async () => {
|
it("Should trigger event modelValue change to new value on when radio button selected", async () => {
|
||||||
// Act
|
// Act
|
||||||
const wrapper = shallowMount(buttonQuestion, setupMocks({}));
|
const wrapper = shallowMount(buttonQuestion, setupMocks({propsData: {groupName: "group-name"}}));
|
||||||
await wrapper.setProps({
|
await wrapper.setProps({
|
||||||
answers: ["2022", "2021", "2020"],
|
answers: ["2022", "2021", "2020"],
|
||||||
isMultiSelect: false,
|
isMultiSelect: false,
|
||||||
|
|
@ -129,6 +134,7 @@ describe("buttonQuestion.vue", () => {
|
||||||
propsData: {
|
propsData: {
|
||||||
modelValue: ["2022", "2021", "2020"],
|
modelValue: ["2022", "2021", "2020"],
|
||||||
isMultiSelect: true,
|
isMultiSelect: true,
|
||||||
|
groupName: "group-name"
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
const val = { checkValue: true, value: "2019", }
|
const val = { checkValue: true, value: "2019", }
|
||||||
|
|
@ -144,7 +150,8 @@ describe("buttonQuestion.vue", () => {
|
||||||
const wrapper = shallowMount(buttonQuestion, setupMocks({
|
const wrapper = shallowMount(buttonQuestion, setupMocks({
|
||||||
propsData: {
|
propsData: {
|
||||||
isMultiSelect: true,
|
isMultiSelect: true,
|
||||||
modelValue: ['a', 'b']
|
modelValue: ['a', 'b'],
|
||||||
|
groupName: "group-name"
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
const val = { checkValue: true, value: "2021", }
|
const val = { checkValue: true, value: "2021", }
|
||||||
|
|
@ -161,7 +168,8 @@ describe("buttonQuestion.vue", () => {
|
||||||
const wrapper = shallowMount(buttonQuestion, setupMocks({
|
const wrapper = shallowMount(buttonQuestion, setupMocks({
|
||||||
propsData: {
|
propsData: {
|
||||||
isMultiSelect: true,
|
isMultiSelect: true,
|
||||||
modelValue: ['a', 'b']
|
modelValue: ['a', 'b'],
|
||||||
|
groupName: "group-name"
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -173,24 +181,6 @@ describe("buttonQuestion.vue", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe("buttonQuestion.vue", () => {
|
|
||||||
it("Should do nothing to this.selectedValues if this.selectedValues is not an array", () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(buttonQuestion, setupMocks({
|
|
||||||
propsData: {
|
|
||||||
isMultiSelect: true,
|
|
||||||
modelValue: 'a',
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
const val = { checkValue: true, value: "c", }
|
|
||||||
wrapper.vm.handleCheckedChanged(val);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(wrapper.vm.selectedValues).toEqual("a");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function setupMocks(mountOptionsMockData = {}) {
|
function setupMocks(mountOptionsMockData = {}) {
|
||||||
const defaultMountOptions = { route: { query: { fmgPage: 'page-name' } } };
|
const defaultMountOptions = { route: { query: { fmgPage: 'page-name' } } };
|
||||||
const baseMountOptions = getMountOptions(Object.assign(defaultMountOptions, mountOptionsMockData));
|
const baseMountOptions = getMountOptions(Object.assign(defaultMountOptions, mountOptionsMockData));
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,26 @@
|
||||||
<!-- Documented in confluence https://safelite.atlassian.net/wiki/spaces/DC/pages/76644418/Button+Question+Component -->
|
<!-- Documented in confluence https://safelite.atlassian.net/wiki/spaces/DC/pages/76644418/Button+Question+Component -->
|
||||||
<template>
|
<template>
|
||||||
<div :class="isOverflowScrollable ? 'button-question button-question-overflow' : 'button-question'">
|
<div :class="isOverflowScrollable ? 'button-question button-question-overflow' : 'button-question'">
|
||||||
<div v-if="questionText" class="question-text d-flex">
|
<div v-if="questionText && answers && answers.length > 0" class="question-text d-flex">
|
||||||
<span class="fs-5 fw-bold w-100">{{ questionText }}</span>
|
<span class="fs-5 fw-bold w-100">{{ questionText }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-100 d-flex justify-content-center">
|
<div class="w-100 d-flex justify-content-center">
|
||||||
<fieldset class="w-100" :aria-required=isRequired :class="getFieldSetClasses" :role="isMultiSelect ? 'group' : 'radiogroup'" :aria-labelledby="groupName">
|
<fieldset class="w-100" :aria-required=isRequired :class="getFieldSetClasses" :role="isMultiSelect ? 'group' : 'radiogroup'" :aria-labelledby="formattedGroupName">
|
||||||
<legend class="sr-only" :data-focus-target="groupName" :id="groupName" tabindex="-1">
|
<legend class="sr-only" :data-focus-target="formattedGroupName" :id="formattedGroupName" tabindex="-1">
|
||||||
{{(isMultiSelect && answers && answers.length > 1) ? 'Select one or more options below.' : 'Select an option below.' }}
|
{{(isMultiSelect && answers && answers.length > 1) ? 'Select one or more options below.' : 'Select an option below.' }}
|
||||||
</legend>
|
</legend>
|
||||||
<div :class="getComponentWrapperClasses">
|
<div :class="getComponentLoopWrapperClasses">
|
||||||
|
<div :class="getComponentWrapperClasses" v-for="answer in answers" :key="answer.Name ? answer.Name : answer">
|
||||||
<component
|
<component
|
||||||
:is="buttonType"
|
:is="buttonType"
|
||||||
v-for="answer in answers"
|
|
||||||
:key="answer.Name ? answer.Name : answer"
|
|
||||||
@isCheckedChanged="handleCheckedChanged"
|
@isCheckedChanged="handleCheckedChanged"
|
||||||
:buttonID="answer.Name ? groupName + '-' + answer.Name : groupName + '-' + answer"
|
:buttonID="answer.Name ? formattedGroupName + '-' + answer.Name : formattedGroupName + '-' + answer"
|
||||||
:value="getValues(answer)"
|
:value="getValues(answer)"
|
||||||
:buttonLabel="answer.Text ? answer.Text : answer"
|
:buttonLabel="answer.Text ? answer.Text : answer"
|
||||||
:buttonLabelSubCopy="answer.SubText"
|
:buttonLabelSubCopy="answer.SubText"
|
||||||
:textPosition="textPosition"
|
:textPosition="textPosition"
|
||||||
:isMultiSelect="isMultiSelect"
|
:isMultiSelect="isMultiSelect"
|
||||||
:groupName="groupName"
|
:groupName="formattedGroupName"
|
||||||
:selectingInitiatesLoad="selectingInitiatesLoad"
|
:selectingInitiatesLoad="selectingInitiatesLoad"
|
||||||
:loaderColor="loaderColor"
|
:loaderColor="loaderColor"
|
||||||
:loaderPosition="loaderPosition"
|
:loaderPosition="loaderPosition"
|
||||||
|
|
@ -38,11 +37,18 @@
|
||||||
:class="[suppressError ? 'alertError' : '']"
|
:class="[suppressError ? 'alertError' : '']"
|
||||||
:clearOnUnmount="clearOnUnmount"
|
:clearOnUnmount="clearOnUnmount"
|
||||||
/>
|
/>
|
||||||
|
<!-- For nested questions -->
|
||||||
|
<transition name="fade" mode="out-in">
|
||||||
|
<div v-if="typeof selectedValues == 'string' && selectedValues == answer.Name">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
<div class="row form-test-error mt-1">
|
<div class="row form-test-error mt-1">
|
||||||
<error-message :name="groupName" v-if="!suppressError"></error-message>
|
<error-message :name="formattedGroupName" v-if="!suppressError"></error-message>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -81,22 +87,31 @@ export default {
|
||||||
isRequired: Boolean,
|
isRequired: Boolean,
|
||||||
isOverflowScrollable: Boolean,
|
isOverflowScrollable: Boolean,
|
||||||
isWide: Boolean,
|
isWide: Boolean,
|
||||||
modelValue: Array,
|
modelValue: [Array, String],
|
||||||
validationRules: String,
|
validationRules: String,
|
||||||
suppressError: Boolean,
|
suppressError: Boolean,
|
||||||
useTextForValue: Boolean,
|
useTextForValue: Boolean,
|
||||||
clearOnUnmount: {
|
clearOnUnmount: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
getFieldSetClasses() {
|
formattedGroupName() {
|
||||||
return this.isOverflowScrollable
|
return this.groupName.replace(" ", "-");
|
||||||
? "container-fluid overflow-scroll position-absolute px-5 pt-1 py-0"
|
|
||||||
: "";
|
|
||||||
},
|
},
|
||||||
getComponentWrapperClasses() {
|
getFieldSetClasses() {
|
||||||
|
if (this.isOverflowScrollable) {
|
||||||
|
return "container-fluid overflow-scroll position-absolute px-5 pt-1 py-0";
|
||||||
|
}
|
||||||
|
else if (this.buttonType == "listCard") {
|
||||||
|
return "w-100";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getComponentLoopWrapperClasses() {
|
||||||
let classes;
|
let classes;
|
||||||
switch (this.buttonType) {
|
switch (this.buttonType) {
|
||||||
case "listButton":
|
case "listButton":
|
||||||
|
|
@ -106,7 +121,7 @@ export default {
|
||||||
classes = "d-flex flex-row p-0";
|
classes = "d-flex flex-row p-0";
|
||||||
break;
|
break;
|
||||||
case 'listCard':
|
case 'listCard':
|
||||||
classes = 'row justify-content-center g-2'
|
classes = "row g-2 justify-content-center";
|
||||||
break;
|
break;
|
||||||
case 'radio':
|
case 'radio':
|
||||||
classes = 'ui-radio d-flex'
|
classes = 'ui-radio d-flex'
|
||||||
|
|
@ -114,11 +129,22 @@ export default {
|
||||||
}
|
}
|
||||||
return classes;
|
return classes;
|
||||||
},
|
},
|
||||||
|
getComponentWrapperClasses() {
|
||||||
|
let classes = "";
|
||||||
|
|
||||||
|
classes += this.isWide ? "col-12" : "col";
|
||||||
|
|
||||||
|
if (this.buttonType == "radio") {
|
||||||
|
classes += " radio-button-container";
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes;
|
||||||
|
},
|
||||||
getColLength(){
|
getColLength(){
|
||||||
if(this.isWide) {
|
if(this.isWide) {
|
||||||
return "12"
|
return "12"
|
||||||
} else {
|
} else {
|
||||||
return this.answers.length < 3 ? '' : '-4';
|
return "";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
selectedValues: {
|
selectedValues: {
|
||||||
|
|
@ -135,6 +161,7 @@ export default {
|
||||||
if (this.useTextForValue){
|
if (this.useTextForValue){
|
||||||
return answer.Text
|
return answer.Text
|
||||||
}
|
}
|
||||||
|
|
||||||
return answer.Name ? answer.Name : answer;
|
return answer.Name ? answer.Name : answer;
|
||||||
},
|
},
|
||||||
handleCheckedChanged(val) {
|
handleCheckedChanged(val) {
|
||||||
|
|
@ -146,7 +173,12 @@ export default {
|
||||||
val.checkValue ? newSelectedValues.push(val.value) : newSelectedValues.splice(newSelectedValues.indexOf(val.value), 1);
|
val.checkValue ? newSelectedValues.push(val.value) : newSelectedValues.splice(newSelectedValues.indexOf(val.value), 1);
|
||||||
this.selectedValues = newSelectedValues;
|
this.selectedValues = newSelectedValues;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
this.selectedValues = val.value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.$emit("isCheckedChanged", val);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
|
@ -172,6 +204,12 @@ export default {
|
||||||
}
|
}
|
||||||
.button-question {
|
.button-question {
|
||||||
color: $black;
|
color: $black;
|
||||||
|
|
||||||
|
.radio-button-container {
|
||||||
|
&:not(:last-child) {
|
||||||
|
padding-bottom: map-get($spacers, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.question-text {
|
.question-text {
|
||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@
|
||||||
<div v-for="(q, i) in questions" :key="i">
|
<div v-for="(q, i) in questions" :key="i">
|
||||||
<transition appear name="fade" mode="out-in">
|
<transition appear name="fade" mode="out-in">
|
||||||
<buttonQuestion
|
<buttonQuestion
|
||||||
v-if="q.questionSequence === currentQuestion"
|
v-if="q.answerSelected || (q.questionSequence === currentQuestionNum)"
|
||||||
class="radioQuestion"
|
class="radioQuestion"
|
||||||
|
:class="(q.questionSequence === currentQuestionNum) && 'current-question'"
|
||||||
:questionText="q.questionText"
|
:questionText="q.questionText"
|
||||||
:answers="q.answers"
|
:answers="q.answers"
|
||||||
:groupName="`${questionData.glassName}-${questionData.glassLocation}-${i}`"
|
:groupName="`${questionData.glassName}-${questionData.glassLocation}-${i}`"
|
||||||
textPosition="text-start"
|
v-model="selectedValuesArray"
|
||||||
v-model="selectedValue"
|
|
||||||
:isRequired=true
|
:isRequired=true
|
||||||
:validationRules="validationRules"
|
:validationRules="validationRules"
|
||||||
:clearOnUnmount=false
|
:clearOnUnmount=false
|
||||||
|
|
@ -24,8 +24,8 @@ export default {
|
||||||
name: "questionChain",
|
name: "questionChain",
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
currentQuestion: 1,
|
currentQuestionNum: 1,
|
||||||
answeredQuestions: [],
|
questions: [{ "BlankObject": "NOT USED... placeholder for question #0"}],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -33,84 +33,109 @@ export default {
|
||||||
validationRules: String,
|
validationRules: String,
|
||||||
modelValue: Array,
|
modelValue: Array,
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
this.questionData.partQuestions.map((q, i) => {
|
||||||
|
let answerPair = [];
|
||||||
|
const eachQuestion = {
|
||||||
|
questionText: q.questionText,
|
||||||
|
questionSequence: q.questionSequence,
|
||||||
|
answers: q.answers.map((a) => {
|
||||||
|
answerPair.push(a.nextQuestionSequence ? a.nextQuestionSequence : a.answerResult);
|
||||||
|
return {
|
||||||
|
Text: a.answerText,
|
||||||
|
// Name will either be nextQuestionSequence or answerResult
|
||||||
|
// Name will be used by list-button as the input value.
|
||||||
|
// It must be a single string or number, so concatenating together a string with
|
||||||
|
// 4 pieces of data separated by pipe characters:
|
||||||
|
// question number|type of answer|answer value|answer text
|
||||||
|
Name: a.nextQuestionSequence ?
|
||||||
|
q.questionSequence + "|nextQuestion|" + a.nextQuestionSequence + "|" + a.answerText :
|
||||||
|
q.questionSequence + "|answer|" + a.answerResult + "|" + a.answerText,
|
||||||
|
nextQuestionSequence: a.nextQuestionSequence,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
answerSelected: "",
|
||||||
|
};
|
||||||
|
eachQuestion.answerPair = answerPair;
|
||||||
|
this.questions.push(eachQuestion);
|
||||||
|
});
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
selectedValuesArray: {
|
||||||
questions() {
|
|
||||||
console.log("answeredQuestions: ", this.answeredQuestions)
|
|
||||||
const questions = this.questionData.partQuestions.map((q, i) => {
|
|
||||||
return {
|
|
||||||
questionText: q.questionText,
|
|
||||||
questionSequence: q.questionSequence,
|
|
||||||
answers: q.answers.map((a) => {
|
|
||||||
return {
|
|
||||||
Text: a.answerText,
|
|
||||||
// Name will either be nextQuestionSequence or answerResult
|
|
||||||
Name: a.nextQuestionSequence ? a.nextQuestionSequence : "answer-" + a.answerResult,
|
|
||||||
nextQuestionSequence: a.nextQuestionSequence,
|
|
||||||
answerResult: a.answerResult,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// add an empty item to be array[0] since we start with 1
|
|
||||||
questions.unshift({ "DeliberatelyBlankObject": "This object has been added as a placeholder only for question #0"});
|
|
||||||
return questions;
|
|
||||||
},
|
|
||||||
selectedValue: {
|
|
||||||
get: function() {
|
get: function() {
|
||||||
return this.modelValue;
|
return [];
|
||||||
},
|
},
|
||||||
set: function(returnedAnswer) {
|
set: function(returnedAnswerArray) {
|
||||||
const isNewModelValueComplete = this.getNewModelValue(returnedAnswer);
|
const returnedAnswer = returnedAnswerArray[returnedAnswerArray.length-1];
|
||||||
|
const isQuestionChainComplete = this.handleReturnedAnswer(returnedAnswer);
|
||||||
|
|
||||||
if (isNewModelValueComplete) {
|
if (isQuestionChainComplete) {
|
||||||
this.$emit("update:modelValue", isNewModelValueComplete);
|
this.$emit("update:modelValue", isQuestionChainComplete);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
currentQuestion() {
|
||||||
|
return this.questions[this.currentQuestionNum];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleReturnedAnswer(returnedAnswer) { // returns either a final answer or Boolean false
|
||||||
|
if (!returnedAnswer) { return false }
|
||||||
|
|
||||||
|
// Example returnedAnswers:
|
||||||
|
// "1|nextQuestion|3|No"
|
||||||
|
// "5|answer|DW02104|Yes"
|
||||||
|
|
||||||
|
const returnedAnswerArray = returnedAnswer.split("|");
|
||||||
|
const questionNum = returnedAnswerArray[0];
|
||||||
|
const questionType = returnedAnswerArray[1];
|
||||||
|
const questionAnswer = returnedAnswerArray[2];
|
||||||
|
const questionAnswerText = returnedAnswerArray[3];
|
||||||
|
|
||||||
|
// remove all previous answers after the index of this one in questions
|
||||||
|
this.questions.map((q) => {
|
||||||
|
if ((q.questionSequence > questionNum) || (q.answerPair?.includes(questionAnswer))) {
|
||||||
|
q.answerSelected = "";
|
||||||
|
}
|
||||||
|
return q;
|
||||||
|
});
|
||||||
|
|
||||||
|
// set this question as "answered"
|
||||||
|
this.questions[questionNum].answerSelected = questionAnswerText;
|
||||||
|
|
||||||
|
// update to next question index
|
||||||
|
this.currentQuestionNum = questionType === "nextQuestion" ? parseInt(questionAnswer) : parseInt(questionNum); // update count to display next question
|
||||||
|
|
||||||
|
// return false if there's a nextQuestion... or return an object with "final" answers
|
||||||
|
if (questionType === "nextQuestion") {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
const answeredQuestions = [];
|
||||||
|
this.questions.forEach(( q ) => {
|
||||||
|
if (q.answerSelected) {
|
||||||
|
answeredQuestions.push({
|
||||||
|
questionText: q.questionText,
|
||||||
|
selectedAnswerText: q.answerSelected,
|
||||||
|
questionNum: questionNum,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
answerResult: questionAnswer,
|
||||||
|
answeredQuestions: answeredQuestions,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
watch: {
|
||||||
getNewModelValue(returnedAnswer) {
|
currentQuestion: {
|
||||||
if (!returnedAnswer || !Array.isArray(returnedAnswer)) { return false }
|
handler() {
|
||||||
const lastAnswer = returnedAnswer[returnedAnswer.length - 1];
|
// scrolls page to next active question
|
||||||
const currentQuestion = this.questions[this.currentQuestion];
|
this.$nextTick(() => {
|
||||||
|
document.querySelector('.current-question').scrollIntoView({behavior: "smooth"});
|
||||||
if (lastAnswer.indexOf("answer-") === 0) {
|
})
|
||||||
// if it is an answerResult
|
},
|
||||||
const finalAnswer = lastAnswer.slice(7);
|
deep: true
|
||||||
|
|
||||||
const currentQuestionSelectedAnswer = currentQuestion.answers.find(
|
|
||||||
({ answerResult }) => answerResult === finalAnswer
|
|
||||||
);
|
|
||||||
|
|
||||||
// add current item to list of answered questions
|
|
||||||
this.answeredQuestions.push(
|
|
||||||
{
|
|
||||||
questionText: currentQuestion.questionText,
|
|
||||||
selectedAnswerText: currentQuestionSelectedAnswer.Text,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
answerResult: finalAnswer,
|
|
||||||
answeredQuestions: this.answeredQuestions,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const currentQuestionSelectedAnswer = currentQuestion.answers.find(
|
|
||||||
({ nextQuestionSequence }) => nextQuestionSequence === parseInt(lastAnswer)
|
|
||||||
);
|
|
||||||
|
|
||||||
// add current item to list of answered questions
|
|
||||||
this.answeredQuestions.push(
|
|
||||||
{
|
|
||||||
questionText: currentQuestion.questionText,
|
|
||||||
selectedAnswerText: currentQuestionSelectedAnswer.Text,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
this.currentQuestion = parseInt(lastAnswer); // update count to display next question
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
|
|
||||||
|
|
@ -197,7 +197,11 @@ export default {
|
||||||
handler() {
|
handler() {
|
||||||
// does this vehicle match the previously selected carId?
|
// does this vehicle match the previously selected carId?
|
||||||
this.isCarIdDifferent = this.selectedVehicle.vehicle.carId !== store.getters.vehicle.carId;
|
this.isCarIdDifferent = this.selectedVehicle.vehicle.carId !== store.getters.vehicle.carId;
|
||||||
this.$refs.funnelFooter.updateButtonText(`Continue with ${this.selectedVehicle.vehicle.year} ${this.selectedVehicle.vehicle.make} ${this.selectedVehicle.vehicle.model}`);
|
if (this.isCarIdDifferent) {
|
||||||
|
this.$refs.funnelFooter.updateButtonText(`Continue with ${this.selectedVehicle.vehicle.year} ${this.selectedVehicle.vehicle.make} ${this.selectedVehicle.vehicle.model}`);
|
||||||
|
} else {
|
||||||
|
this.$refs.funnelFooter.updateButtonText(this.getCmsContent("FunnelFooterWidget", "ForwardButtonText"));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
deep: true
|
deep: true
|
||||||
},
|
},
|
||||||
|
|
|
||||||
10
src/layouts/quote/quote.vue
Normal file
10
src/layouts/quote/quote.vue
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "quote",
|
||||||
|
methods: {
|
||||||
|
arePagePrerequisitesValid() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -162,10 +162,6 @@ export default {
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
resetDependentState() {
|
|
||||||
store.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
|
|
||||||
},
|
|
||||||
|
|
||||||
attachCustomEvents(){
|
attachCustomEvents(){
|
||||||
if(this.$store.getters.vehicle.imageVifNumber){
|
if(this.$store.getters.vehicle.imageVifNumber){
|
||||||
this.pushEventToGA(this.GaCategories.EVOX, `${this.GaActions.VIF}_${this.$store.getters.vehicle.imageVifNumber}`,
|
this.pushEventToGA(this.GaCategories.EVOX, `${this.GaActions.VIF}_${this.$store.getters.vehicle.imageVifNumber}`,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
:answers="makes"
|
:answers="makes"
|
||||||
groupName="ChooseVehicleMake"
|
groupName="ChooseVehicleMake"
|
||||||
textPosition="text-start"
|
textPosition="text-start"
|
||||||
v-model="selectedValueAsArray"
|
v-model="selectedValue"
|
||||||
isRequired=true
|
isRequired=true
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -34,14 +34,13 @@ export default {
|
||||||
questionText(){
|
questionText(){
|
||||||
return this.getCmsContent(this.cmsWidgetName, 'QuestionText');
|
return this.getCmsContent(this.cmsWidgetName, 'QuestionText');
|
||||||
},
|
},
|
||||||
selectedValueAsArray: {
|
selectedValue: {
|
||||||
get: function() {
|
get: function() {
|
||||||
const modelValueAsArray = this.modelValue ? [this.modelValue] : [];
|
const modelValueAsArray = this.modelValue ? [this.modelValue] : [];
|
||||||
return modelValueAsArray;
|
return modelValueAsArray;
|
||||||
},
|
},
|
||||||
set: function(newValue) {
|
set: function(newValue) {
|
||||||
const newValueAsScalar = newValue && newValue.length > 0 ? newValue[0] : null;
|
this.$emit("update:modelValue", newValue);
|
||||||
this.$emit("update:modelValue", newValueAsScalar);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
:answers="models"
|
:answers="models"
|
||||||
groupName="ChooseVehicleModel"
|
groupName="ChooseVehicleModel"
|
||||||
textPosition="text-start"
|
textPosition="text-start"
|
||||||
v-model="selectedValueAsArray"
|
v-model="selectedValue"
|
||||||
isRequired=true
|
isRequired=true
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -34,14 +34,13 @@ export default {
|
||||||
questionText(){
|
questionText(){
|
||||||
return this.getCmsContent(this.cmsWidgetName, 'QuestionText');
|
return this.getCmsContent(this.cmsWidgetName, 'QuestionText');
|
||||||
},
|
},
|
||||||
selectedValueAsArray: {
|
selectedValue: {
|
||||||
get: function() {
|
get: function() {
|
||||||
const modelValueAsArray = this.modelValue ? [this.modelValue] : [];
|
const modelValueAsArray = this.modelValue ? [this.modelValue] : [];
|
||||||
return modelValueAsArray;
|
return modelValueAsArray;
|
||||||
},
|
},
|
||||||
set: function(newValue) {
|
set: function(newValue) {
|
||||||
const newValueAsScalar = newValue && newValue.length > 0 ? newValue[0] : null;
|
this.$emit("update:modelValue", newValue);
|
||||||
this.$emit("update:modelValue", newValueAsScalar);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -69,24 +69,25 @@ describe("glass-part-question.vue", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Should emit updateModelValue, and have correct attributes", async () => {
|
test("Should emit updateModelValue, and have correct attributes", async () => {
|
||||||
|
|
||||||
//Arrange
|
//Arrange
|
||||||
|
|
||||||
const { wrapper } = setupMocks(featureListData);
|
const { wrapper } = setupMocks(featureListData);
|
||||||
|
store.getters.pageData.mockReset();
|
||||||
|
store.getters.pageData.mockReturnValueOnce({ partsOrQuestions: [{glassName: "Stationary", glassLocation: "Rear", parts: [{ partNumber: "DB12209GTYN", color: "Green Tint"}]}] });
|
||||||
|
|
||||||
//Act
|
//Act
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
const listCard = await wrapper.findComponent({
|
const listCard = await wrapper.findComponent({
|
||||||
name: "listCard",
|
name: "buttonQuestion",
|
||||||
});
|
});
|
||||||
|
|
||||||
wrapper.setValue({ selectedTint: 'Green Tint' });
|
await wrapper.setData({ selectedTint: 'Green Tint' });
|
||||||
|
// to trigger the computed setter
|
||||||
|
wrapper.vm.selectedPartNumber = "DB12209GTYN";
|
||||||
|
|
||||||
//Assert
|
//Assert
|
||||||
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([{ selectedTint: 'Green Tint' }]);
|
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([{ partNumber: "DB12209GTYN", color: "Green Tint"}]);
|
||||||
expect(listCard.attributes("buttonid")).toBe("Rear-Stationary-Green Tint");
|
|
||||||
expect(listCard.attributes("groupname")).toBe("Rear-Stationary");
|
expect(listCard.attributes("groupname")).toBe("Rear-Stationary");
|
||||||
expect(listCard.attributes("isradio")).toBe("true");
|
expect(listCard.attributes("validationrules")).toBe("Rear-Stationary-tint-required");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ResetTintAndPartSelections, should reset data elements ", async () => {
|
test("ResetTintAndPartSelections, should reset data elements ", async () => {
|
||||||
|
|
@ -96,21 +97,101 @@ describe("glass-part-question.vue", () => {
|
||||||
|
|
||||||
//Act
|
//Act
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
wrapper.setData({ selectedTint: { "Rear-Stationary": 'Green Tint' } });
|
await wrapper.setData({ selectedTint: 'Green Tint', selectedPartNumber: "test" });
|
||||||
|
|
||||||
expect(wrapper.vm.selectedTint).toStrictEqual({ "Rear-Stationary": 'Green Tint' });
|
expect(wrapper.vm.selectedTint).toEqual('Green Tint');
|
||||||
|
expect(wrapper.vm.selectedPartNumber).toEqual('test');
|
||||||
|
|
||||||
await wrapper.vm.ResetTintAndPartSelections();
|
await wrapper.vm.ResetTintAndPartSelections();
|
||||||
expect(wrapper.vm.selectedTint).toStrictEqual({});
|
|
||||||
|
expect(wrapper.vm.selectedTint).toEqual("Green Tint");
|
||||||
|
expect(wrapper.vm.selectedPartNumber).toEqual(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("default is selected if only one option", async () => {
|
||||||
|
// Arrange
|
||||||
|
store.getters.pageData.mockReset();
|
||||||
|
store.getters.pageData.mockReturnValueOnce({ partsOrQuestions: [{glassName: "Stationary", glassLocation: "Rear", parts: [{ partNumber: "DB12209GTYN", color: "Green Tint"}]}] });
|
||||||
|
const { wrapper } = setupMocks(featureListData);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
await wrapper.setData({ selectedTint: "Green Tint" });
|
||||||
|
// take emitted value, pass down as modelValue
|
||||||
|
// yes, yes, it's not ideal
|
||||||
|
await wrapper.setProps({ modelValue: wrapper.emitted()["update:modelValue"][0][0] })
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.vm.selectedPartNumber).toBe("DB12209GTYN");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("default is not selected if more than one option", async () => {
|
||||||
|
// Arrange
|
||||||
|
store.getters.pageData.mockReset();
|
||||||
|
store.getters.pageData.mockReturnValueOnce({ partsOrQuestions: [{glassName: "Stationary", glassLocation: "Rear", parts: [{ partNumber: "DB12209GTYN", color: "Green Tint"}, { partNumber: "DB12209GTYNXXX", color: "Green Tint"}]}] });
|
||||||
|
const { wrapper } = setupMocks(featureListData);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
await wrapper.setData({ selectedTint: "Green Tint" });
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.emitted()["update:modelValue"]).toBeFalsy();
|
||||||
|
expect(wrapper.vm.selectedPartNumber).toBeFalsy();
|
||||||
|
})
|
||||||
|
|
||||||
|
const partsForSelectedTintTestCases = [
|
||||||
|
["Rear", "Stationary", "Green Tint", [{ partNumber: "Glass1", color: "Green Tint"}, { partNumber: "Glass3", color: "Green Tint"}, { partNumber: "Glass4", color: "Green Tint"}, { partNumber: "Glass6", color: "Green Tint"} ]],
|
||||||
|
["Rear", "Stationary", "Blue Tint", [{ partNumber: "Glass2", color: "Blue Tint"}, { partNumber: "Glass5", color: "Blue Tint"}]],
|
||||||
|
["Rear", "Stationary", "Red Tint", [{ partNumber: "Glass7", color: "Red Tint"}]],
|
||||||
|
["Windshield", "Single", "Green Tint", [{ partNumber: "Windshield1", color: "Green Tint"}, { partNumber: "Windshield2", color: "Green Tint"}]],
|
||||||
|
["Windshield", "Single", "Blue Tint", []],
|
||||||
|
["Driver", "Quarter", "Green Tint", []]
|
||||||
|
];
|
||||||
|
test.each(partsForSelectedTintTestCases)("partsForSelectedTint returns correct parts", async (glassLocation, glassName, selectedTint, expectedResults) => {
|
||||||
|
// Arrange
|
||||||
|
store.getters.pageData.mockReset();
|
||||||
|
store.getters.pageData.mockReturnValueOnce({ partsOrQuestions: [
|
||||||
|
{
|
||||||
|
glassName: "Stationary",
|
||||||
|
glassLocation: "Rear",
|
||||||
|
parts: [
|
||||||
|
{ partNumber: "Glass1", color: "Green Tint"},
|
||||||
|
{ partNumber: "Glass2", color: "Blue Tint"},
|
||||||
|
{ partNumber: "Glass3", color: "Green Tint"},
|
||||||
|
{ partNumber: "Glass4", color: "Green Tint"},
|
||||||
|
{ partNumber: "Glass5", color: "Blue Tint"},
|
||||||
|
{ partNumber: "Glass6", color: "Green Tint"},
|
||||||
|
{ partNumber: "Glass7", color: "Red Tint"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
glassName: "Single",
|
||||||
|
glassLocation: "Windshield",
|
||||||
|
parts: [{ partNumber: "Windshield1", color: "Green Tint"}, { partNumber: "Windshield2", color: "Green Tint"}]
|
||||||
|
}
|
||||||
|
]});
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
glassLocationProp: glassLocation,
|
||||||
|
glassNameProp: glassName,
|
||||||
|
colorAnswersProp: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await wrapper.setData({ selectedTint: selectedTint });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(expectedResults).toEqual(wrapper.vm.partsForSelectedTint);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
function setupMocks({ glassNameProp, glassLocationProp, colorAnswersProp, modelValueProp }) {
|
|
||||||
|
|
||||||
|
function setupMocks({ glassNameProp, glassLocationProp, colorAnswersProp, modelValueProp }) {
|
||||||
//Mock store
|
//Mock store
|
||||||
store.getters.pageData.mockReturnValueOnce({ partsOrQuestions: {}});
|
store.getters.pageData.mockReturnValueOnce({ partsOrQuestions: [{glassName: "Stationary", glassLocation: "Rear", parts: []}] });
|
||||||
store.getters.lineItems = { glassParts: {} }
|
store.getters.lineItems = { glassParts: {} }
|
||||||
|
|
||||||
const mountOptions = getMountOptions({
|
const mountOptions = getMountOptions({
|
||||||
|
|
|
||||||
|
|
@ -1,245 +1,230 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p class="mb-0 color-question-text">{{ colorQuestionText }}</p>
|
<p class="mb-0 color-question-text">{{ colorQuestionText }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="nested-radio">
|
||||||
|
<div class="row my-2">
|
||||||
<div
|
<div class="col">
|
||||||
class="container nested-radio"
|
<buttonQuestion
|
||||||
v-for="(value, name, index) in featureListData"
|
v-model="selectedTint"
|
||||||
:key="index"
|
:answers="tintSelectionOptions"
|
||||||
>
|
buttonType="listCard"
|
||||||
<div class="row my-2">
|
:isWide="true"
|
||||||
<div class="col">
|
altText=""
|
||||||
<listCard
|
isRequired
|
||||||
v-model="selectedTint[name]"
|
:groupName="`${glassLocation}-${glassName}`"
|
||||||
:value="`${glassLocation}-${glassName}-${name}`"
|
@isCheckedChanged="ResetTintAndPartSelections"
|
||||||
:isRadio="true"
|
:validationRules="tintValidationRules"
|
||||||
:isWide="true"
|
>
|
||||||
:buttonImage="
|
<div class="row my-2" aria-live="polite">
|
||||||
require(`@/assets/img/tints/${getTintSourceImage(
|
<div class="col">
|
||||||
glassLocation,
|
<buttonQuestion
|
||||||
name
|
v-model="selectedPartNumber"
|
||||||
)}`)
|
buttonType="radio"
|
||||||
"
|
class="radioQuestion"
|
||||||
:buttonLabel="name"
|
:questionText="glassFeatureQuestion"
|
||||||
altText=""
|
:answers="featureListData[selectedTint]"
|
||||||
isRequired
|
textPosition="text-start"
|
||||||
:buttonID="`${glassLocation}-${glassName}-${name}`"
|
:loaderEnabled="false"
|
||||||
:groupName="`${glassLocation}-${glassName}`"
|
isRequired
|
||||||
@isCheckedChanged="ResetTintAndPartSelections()"
|
:groupName="`${glassLocation}-${glassName}-${selectedTint}`"
|
||||||
:validationRules="validationRules"
|
:validationRules="partValidationRules"
|
||||||
/>
|
/>
|
||||||
<div class="row form-test-error mt-1">
|
</div>
|
||||||
<error-message :name="`${glassLocation}-${glassName}`" v-if="!suppressError"></error-message>
|
</div>
|
||||||
|
</buttonQuestion>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<transition name="fade" mode="out-in">
|
|
||||||
<div
|
|
||||||
v-if="
|
|
||||||
selectedTint[name] != undefined &&
|
|
||||||
selectedTint[name].buttonId ===
|
|
||||||
`${glassLocation}-${glassName}-${name}`
|
|
||||||
"
|
|
||||||
class="row my-2"
|
|
||||||
aria-live="polite"
|
|
||||||
>
|
|
||||||
<div class="col">
|
|
||||||
<buttonQuestion
|
|
||||||
v-model="selectedPart[glassLocation]"
|
|
||||||
buttonType="radio"
|
|
||||||
class="radioQuestion"
|
|
||||||
:questionText="glassFeatureQuestion"
|
|
||||||
:answers="value"
|
|
||||||
textPosition="text-start"
|
|
||||||
:loaderEnabled="false"
|
|
||||||
isRequired
|
|
||||||
:groupName="`${glassLocation}-${glassName}-${name}`"
|
|
||||||
:validationRules="validationRules"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Components
|
// Components
|
||||||
import listCard from "@/ux-components/list-card/list-card";
|
|
||||||
import buttonQuestion from "@/common-components/button-question/button-question";
|
import buttonQuestion from "@/common-components/button-question/button-question";
|
||||||
|
|
||||||
// Supporting files
|
// Supporting files
|
||||||
import { getTintImage } from "@/constants/tint-mapper";
|
import { getTintImage } from "@/constants/tint-mapper";
|
||||||
import { getCustomTransformValue } from "@/constants/dynamictext-mapper";
|
import { getCustomTransformValue } from "@/constants/dynamictext-mapper";
|
||||||
import { ErrorMessage } from 'vee-validate';
|
import { defineRule } from "vee-validate";
|
||||||
|
import { required } from "@/helpers/validation-rules";
|
||||||
|
import { errorMessages } from "@/constants/error-messages";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "glass-part-question",
|
name: "glass-part-question",
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
glassColorQuestion: "",
|
glassColorQuestion: "",
|
||||||
glassFeatureQuestion: "",
|
glassFeatureQuestion: "",
|
||||||
selectedTint: {}, // v-model for the list card selections
|
selectedTint: "",
|
||||||
};
|
};
|
||||||
},
|
|
||||||
props: {
|
|
||||||
glassName: String,
|
|
||||||
glassLocation: String,
|
|
||||||
colorAnswers: Array,
|
|
||||||
modelValue: Object,
|
|
||||||
validationRules: String,
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.LoadPreselectedValues();
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
listCard,
|
|
||||||
buttonQuestion,
|
|
||||||
ErrorMessage,
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
colorQuestionText() {
|
|
||||||
return getCustomTransformValue(
|
|
||||||
this.glassColorQuestion,
|
|
||||||
`${this.glassLocation} ${this.glassName}`
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
selectedPart: {
|
glassName: String,
|
||||||
get: function () {
|
glassLocation: String,
|
||||||
return this.modelValue;
|
colorAnswers: Array,
|
||||||
},
|
modelValue: Object,
|
||||||
set: function (newValue) {
|
alreadyPopulatedPartsData: Array
|
||||||
this.$emit("update:modelValue", newValue);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
// Creates a map of the feature list data in the correct Name/Value
|
this.LoadPreselectedValues();
|
||||||
// format for the button-question component.
|
|
||||||
featureListData() {
|
|
||||||
const tintMapByColor = this.colorAnswers.reduce((arr, item) => {
|
|
||||||
// Create the key
|
|
||||||
const itemColor = item.ColorAnswerText;
|
|
||||||
arr[itemColor] = arr[itemColor] || [];
|
|
||||||
|
|
||||||
// Map the data to Name/Text object for ButtonQuestion
|
|
||||||
const mappedItem = item.FeatureAnswers.reduce((featureArr, item) => {
|
|
||||||
featureArr["Text"] = item.FeatureAnswerText; // Display to User
|
|
||||||
featureArr["Name"] = item.PartNumber; // Backing Value
|
|
||||||
|
|
||||||
return featureArr;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
// Add onto the final object
|
|
||||||
arr[itemColor].push(mappedItem);
|
|
||||||
|
|
||||||
return arr;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return tintMapByColor;
|
|
||||||
},
|
},
|
||||||
|
components: {
|
||||||
PartDataFromApi() {
|
buttonQuestion,
|
||||||
return this.$store.getters.pageData(this.$route.query.fmgPage);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
// Initialize the component data
|
|
||||||
initializeComponent(cmsContent) {
|
|
||||||
this.glassColorQuestion = cmsContent.ColorQuestionWidget.QuestionText;
|
|
||||||
this.glassFeatureQuestion = cmsContent.FeatureQuestionWidget.QuestionText;
|
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
tintValidationRules() {
|
||||||
|
const validationRuleName = `${this.glassLocation}-${this.glassName}-tint-required`;
|
||||||
|
defineRule(validationRuleName, required(errorMessages.OPTION_REQUIRED));
|
||||||
|
return validationRuleName;
|
||||||
|
},
|
||||||
|
partValidationRules() {
|
||||||
|
const validationRuleName = `${this.glassLocation}-${this.glassName}-part-required`;
|
||||||
|
defineRule(validationRuleName, required(errorMessages.OPTION_REQUIRED));
|
||||||
|
return validationRuleName;
|
||||||
|
},
|
||||||
|
colorQuestionText() {
|
||||||
|
return getCustomTransformValue(
|
||||||
|
this.glassColorQuestion,
|
||||||
|
`${this.glassLocation} ${this.glassName}`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
// Gets tint images based on the glass type, and tint name.
|
tintSelectionOptions() {
|
||||||
// Returns an empty string if the src or object is undefined.
|
let tintOptions = [];
|
||||||
getTintSourceImage(glassLocation, tintColor) {
|
Object.keys(this.featureListData).forEach((tintOption) => {
|
||||||
const tintSourceObject = getTintImage(glassLocation, tintColor);
|
tintOptions.push({
|
||||||
|
Name: tintOption,
|
||||||
if (tintSourceObject === undefined || tintSourceObject.src == undefined) {
|
Text: tintOption,
|
||||||
return "";
|
AnswerImageUrl: require(`@/assets/img/tints/${this.getTintSourceImage(
|
||||||
}
|
this.glassLocation,
|
||||||
|
tintOption
|
||||||
return tintSourceObject.src;
|
)}`),
|
||||||
},
|
});
|
||||||
|
|
||||||
// Reset selections when tint changes for the same glass to ensure proper selection.
|
|
||||||
// Also checks if only a single part is present for the tint.
|
|
||||||
ResetTintAndPartSelections() {
|
|
||||||
this.selectedTint = {};
|
|
||||||
this.selectedPart = {};
|
|
||||||
this.AutoSelectIfSinglePart();
|
|
||||||
},
|
|
||||||
|
|
||||||
// Check if only a single part is present for the tint and set the v-model if it is.
|
|
||||||
AutoSelectIfSinglePart() {
|
|
||||||
// Check if the selected tint only has a single feature
|
|
||||||
Object.keys(this.PartDataFromApi.partsOrQuestions ?? {}).forEach(
|
|
||||||
(key) => {
|
|
||||||
const currentGlassSelection =
|
|
||||||
this.PartDataFromApi.partsOrQuestions[key];
|
|
||||||
|
|
||||||
if (
|
|
||||||
currentGlassSelection.glassName === this.glassName &&
|
|
||||||
currentGlassSelection.glassLocation === this.glassLocation
|
|
||||||
) {
|
|
||||||
if (currentGlassSelection.parts.length === 1) {
|
|
||||||
this.selectedPart = {
|
|
||||||
[currentGlassSelection.glassLocation]: [
|
|
||||||
currentGlassSelection.parts[0].partNumber,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Loads the preselected values from the store.
|
|
||||||
LoadPreselectedValues() {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
if (this.modelValue !== undefined) {
|
|
||||||
// Populate button-question model-value if parts data already exists in VueX
|
|
||||||
const alreadyPopulatedPartsData =
|
|
||||||
this.$store.getters.lineItems.glassParts;
|
|
||||||
|
|
||||||
Object.keys(alreadyPopulatedPartsData).forEach((key) => {
|
|
||||||
const partNumber = alreadyPopulatedPartsData[key].partNumber;
|
|
||||||
const tintColor = alreadyPopulatedPartsData[key].color;
|
|
||||||
|
|
||||||
Object.keys(this.modelValue).forEach((key) => {
|
|
||||||
if (this.modelValue[key][0] === partNumber) {
|
|
||||||
this.selectedTint[tintColor] = {
|
|
||||||
buttonId: `${this.glassLocation}-${this.glassName}-${tintColor}`,
|
|
||||||
checkValue: "",
|
|
||||||
value: `${this.glassLocation}-${this.glassName}-${tintColor}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
return tintOptions;
|
||||||
});
|
},
|
||||||
|
|
||||||
|
selectedPartNumber: {
|
||||||
|
get() {
|
||||||
|
return this.modelValue?.partNumber;
|
||||||
|
},
|
||||||
|
set(newValue) {
|
||||||
|
this.$emit("update:modelValue", this.partsForSelectedTint.filter(part => part.partNumber == newValue)[0]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
partsForSelectedTint() {
|
||||||
|
const matchingGlass = this.PartDataFromApi.partsOrQuestions?.filter(dataForGlassLocationAndName =>
|
||||||
|
dataForGlassLocationAndName.glassName == this.glassName &&
|
||||||
|
dataForGlassLocationAndName.glassLocation == this.glassLocation);
|
||||||
|
const matchingGlassParts = matchingGlass?.length == 1 ? matchingGlass[0].parts : [];
|
||||||
|
return matchingGlassParts.filter(part => part.color == this.selectedTint) ?? [];
|
||||||
|
},
|
||||||
|
|
||||||
|
// Creates a map of the feature list data in the correct Name/Value
|
||||||
|
// format for the button-question component.
|
||||||
|
featureListData() {
|
||||||
|
const tintMapByColor = this.colorAnswers.reduce((arr, item) => {
|
||||||
|
// Create the key
|
||||||
|
const itemColor = item.ColorAnswerText;
|
||||||
|
arr[itemColor] = arr[itemColor] || [];
|
||||||
|
|
||||||
|
// Map the data to Name/Text object for ButtonQuestion
|
||||||
|
const mappedItem = item.FeatureAnswers.reduce(
|
||||||
|
(featureArr, item) => {
|
||||||
|
featureArr["Text"] = item.FeatureAnswerText; // Display to User
|
||||||
|
featureArr["Name"] = item.PartNumber; // Backing Value
|
||||||
|
|
||||||
|
return featureArr;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add onto the final object
|
||||||
|
arr[itemColor].push(mappedItem);
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return tintMapByColor;
|
||||||
|
},
|
||||||
|
|
||||||
|
PartDataFromApi() {
|
||||||
|
return this.$store.getters.pageData(this.$route.query.fmgPage) ?? {};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
methods: {
|
||||||
|
// Initialize the component data
|
||||||
|
initializeComponent(cmsContent) {
|
||||||
|
this.glassColorQuestion =
|
||||||
|
cmsContent.ColorQuestionWidget.QuestionText;
|
||||||
|
this.glassFeatureQuestion =
|
||||||
|
cmsContent.FeatureQuestionWidget.QuestionText;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Gets tint images based on the glass type, and tint name.
|
||||||
|
// Returns an empty string if the src or object is undefined.
|
||||||
|
getTintSourceImage(glassLocation, tintColor) {
|
||||||
|
const tintSourceObject = getTintImage(glassLocation, tintColor);
|
||||||
|
|
||||||
|
if (
|
||||||
|
tintSourceObject === undefined ||
|
||||||
|
tintSourceObject.src == undefined
|
||||||
|
) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return tintSourceObject.src;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Reset selections when tint changes for the same glass to ensure proper selection.
|
||||||
|
// Also checks if only a single part is present for the tint.
|
||||||
|
ResetTintAndPartSelections() {
|
||||||
|
this.selectedPartNumber = null;
|
||||||
|
this.AutoSelectIfSinglePart();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Check if only a single part is present for the tint and set the v-model if it is.
|
||||||
|
AutoSelectIfSinglePart() {
|
||||||
|
if (this.partsForSelectedTint?.length == 1) {
|
||||||
|
this.selectedPartNumber = this.partsForSelectedTint[0].partNumber;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Loads the preselected values from the store.
|
||||||
|
LoadPreselectedValues() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.modelValue !== undefined) {
|
||||||
|
// Populate button-question model-value if parts data already exists in VueX
|
||||||
|
this.selectedTint = this.alreadyPopulatedPartsData?.filter(part => part.partNumber === this.selectedPartNumber)[0].color;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
selectedTint() {
|
||||||
|
this.AutoSelectIfSinglePart();
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.nested-radio {
|
.nested-radio {
|
||||||
.ui-radio {
|
.ui-radio {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin: 0.25rem 0;
|
margin: 0.25rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-question-text {
|
.color-question-text {
|
||||||
color: $black;
|
color: $black;
|
||||||
font-weight: $font-weight-bold;
|
font-weight: $font-weight-bold;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ describe("vehicle-parts.vue", () => {
|
||||||
test("Set cms content called on load", async (done) => {
|
test("Set cms content called on load", async (done) => {
|
||||||
//Arrange
|
//Arrange
|
||||||
store.getters.pageData.mockReturnValue(basePartResponse);
|
store.getters.pageData.mockReturnValue(basePartResponse);
|
||||||
store.getters.lineItems = { glassParts: {} }
|
store.getters.lineItems = { glassParts: null }
|
||||||
|
|
||||||
const { wrapper, apiPromise } = setupMocks(
|
const { wrapper, apiPromise } = setupMocks(
|
||||||
{
|
{
|
||||||
|
|
@ -104,7 +104,7 @@ describe("vehicle-parts.vue", () => {
|
||||||
|
|
||||||
//Arrange
|
//Arrange
|
||||||
store.getters.pageData.mockReturnValue(basePartResponse);
|
store.getters.pageData.mockReturnValue(basePartResponse);
|
||||||
store.getters.lineItems = { glassParts: {} }
|
store.getters.lineItems = { glassParts: null }
|
||||||
|
|
||||||
const { wrapper } = setupMocks({
|
const { wrapper } = setupMocks({
|
||||||
mountOptionsMockData: {
|
mountOptionsMockData: {
|
||||||
|
|
@ -141,7 +141,7 @@ describe("vehicle-parts.vue", () => {
|
||||||
|
|
||||||
//Arrange
|
//Arrange
|
||||||
store.getters.pageData.mockReturnValueOnce(basePartResponse);
|
store.getters.pageData.mockReturnValueOnce(basePartResponse);
|
||||||
store.getters.lineItems = { glassParts: { 0: { partNumber: 'DB12209YPYNOEM'} } }
|
store.getters.lineItems = { glassParts: [{ partNumber: 'DB12209YPYNOEM'} ] }
|
||||||
|
|
||||||
const { wrapper } = setupMocks({
|
const { wrapper } = setupMocks({
|
||||||
mountOptionsMockData: {
|
mountOptionsMockData: {
|
||||||
|
|
@ -177,7 +177,7 @@ describe("vehicle-parts.vue", () => {
|
||||||
|
|
||||||
//Arrange
|
//Arrange
|
||||||
store.getters.pageData.mockReturnValueOnce(basePartResponse);
|
store.getters.pageData.mockReturnValueOnce(basePartResponse);
|
||||||
store.getters.lineItems = { glassParts: {} }
|
store.getters.lineItems = { glassParts: null }
|
||||||
|
|
||||||
const { wrapper } = setupMocks({
|
const { wrapper } = setupMocks({
|
||||||
mountOptionsMockData: {
|
mountOptionsMockData: {
|
||||||
|
|
@ -236,7 +236,7 @@ describe("vehicle-parts.vue", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
wrapper.setData({ glassParts: { "Rear-Stationary": { "Rear": ['DB12209YPYNOEM'] } } });
|
wrapper.setData({ glassParts: {"Rear-Stationary": { partNumber: 'DB12209GTYN'}}});
|
||||||
|
|
||||||
//Act
|
//Act
|
||||||
vehicleParts.beforeRouteEnter.call(
|
vehicleParts.beforeRouteEnter.call(
|
||||||
|
|
@ -280,13 +280,14 @@ function setupMocks({ pageHeaderWidgetHeaderText = {}, mountOptionsMockData = {}
|
||||||
settleAllPromises.mockImplementation(() => apiPromise);
|
settleAllPromises.mockImplementation(() => apiPromise);
|
||||||
fetchCmsContentForPage.mockImplementation(() => Promise.resolve());
|
fetchCmsContentForPage.mockImplementation(() => Promise.resolve());
|
||||||
|
|
||||||
const mountOptions = getMountOptions(mountOptionsMockData,);
|
const mountOptions = getMountOptions(mountOptionsMockData);
|
||||||
const wrapper = shallowMount(vehicleParts, mountOptions);
|
const wrapper = shallowMount(vehicleParts, mountOptions);
|
||||||
|
|
||||||
const partQuestionRearWrapper = wrapper.findComponent({ name: "glassPartQuestion", });
|
const partQuestionRearWrapper = wrapper.findComponent({ name: "glassPartQuestion", });
|
||||||
partQuestionRearWrapper.vm.initializeComponent = glassPartQuestion.methods.initializeComponent;
|
partQuestionRearWrapper.vm.initializeComponent = glassPartQuestion.methods.initializeComponent;
|
||||||
|
|
||||||
wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent;
|
wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent;
|
||||||
|
wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn();
|
||||||
|
|
||||||
return { wrapper, apiPromise };
|
return { wrapper, apiPromise };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,51 @@
|
||||||
<template>
|
<template>
|
||||||
<Form
|
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm">
|
||||||
@submit="onSubmit"
|
<div class="page-container-grouped-styles vehicle-parts">
|
||||||
@invalid-submit="onInvalidSubmit"
|
<funnelHeader ref="funnelHeader" cmsWidgetName="FunnelHeaderWidget" />
|
||||||
ref="theForm"
|
<vehicleBanner
|
||||||
v-slot="{ meta }"
|
ref="vehicleBanner"
|
||||||
>
|
cmsWidgetName="VehicleBannerWidget"
|
||||||
<div class="page-container-grouped-styles vehicle-parts">
|
:displayGenericVehicleImage="false"
|
||||||
<funnelHeader ref="funnelHeader" cmsWidgetName="FunnelHeaderWidget" />
|
/>
|
||||||
<vehicleBanner ref="vehicleBanner" cmsWidgetName="VehicleBannerWidget" :displayGenericVehicleImage="false" />
|
<funnelSubHeader
|
||||||
<funnelSubHeader ref="funnelSubHeader" cmsWidgetName="FunnelSubHeaderWidget" />
|
ref="funnelSubHeader"
|
||||||
<div class="fade-on-route-transition sub-container make-tall">
|
cmsWidgetName="FunnelSubHeaderWidget"
|
||||||
<div class="container-fluid prevent-squish my-5">
|
/>
|
||||||
<div class="row">
|
<div class="fade-on-route-transition sub-container make-tall">
|
||||||
<div class="col">
|
<div class="prevent-squish my-5">
|
||||||
<alert
|
<div class="row">
|
||||||
class="rounded border-0 shadow-sm"
|
<div class="col">
|
||||||
alertClass="alert-warning"
|
<alert
|
||||||
cmsWidgetName="AlertWidget"
|
class="rounded border-0 shadow-sm"
|
||||||
:isDismissible="false"
|
alertClass="alert-warning"
|
||||||
/>
|
cmsWidgetName="AlertWidget"
|
||||||
|
:isDismissible="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div v-for="(item, i) in PartsOrQuestions" :key="i">
|
||||||
|
<!-- Render horizontal lines if there is multi-glass (aka if i > 0) -->
|
||||||
<div v-for="(item, i) in PartsForQuestions" :key="i">
|
|
||||||
<!-- Render horizontal lines if there is multi-glass (aka if i > 0) -->
|
|
||||||
<div class="container-fluid">
|
|
||||||
<hr v-if="i > 0" />
|
<hr v-if="i > 0" />
|
||||||
|
<glassPartQuestion
|
||||||
|
:ref="`${RefPrefix}-${item.glassLocation}-${item.glassName}`"
|
||||||
|
v-model="glassParts[item.glassLocation + '-' + item.glassName]"
|
||||||
|
:glassLocation="item.glassLocation"
|
||||||
|
:glassName="item.glassName"
|
||||||
|
:colorAnswers="item.colorAnswers"
|
||||||
|
:alreadyPopulatedPartsData="alreadyPopulatedPartsData"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<funnelFooter
|
||||||
<glassPartQuestion
|
cmsWidgetName="FunnelFooterWidget"
|
||||||
:ref="`${RefPrefix}-${item.glassLocation}-${item.glassName}`"
|
ref="funnelFooter"
|
||||||
v-model="glassParts[item.glassLocation + '-' + item.glassName]"
|
:isForwardActionDisabled="isForwardActionDisabled"
|
||||||
:glassLocation="item.glassLocation"
|
@back-clicked="backButtonAction"
|
||||||
:glassName="item.glassName"
|
@ForwardClicked="forwardButtonAction"
|
||||||
:colorAnswers="item.colorAnswers"
|
|
||||||
validationRules="replace-options-required"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<funnelFooter
|
|
||||||
cmsWidgetName="FunnelFooterWidget"
|
|
||||||
ref="funnelFooter"
|
|
||||||
:isForwardActionDisabled="!meta.valid"
|
|
||||||
@back-clicked="backButtonAction"
|
|
||||||
@ForwardClicked="forwardButtonAction"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Form>
|
||||||
</Form>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -69,19 +67,18 @@ import { required } from "@/helpers/validation-rules";
|
||||||
import { errorMessages } from "@/constants/error-messages";
|
import { errorMessages } from "@/constants/error-messages";
|
||||||
import { storeActions } from "@/constants/store-actions";
|
import { storeActions } from "@/constants/store-actions";
|
||||||
|
|
||||||
// DEFINE VALIDATION RULES
|
|
||||||
defineRule("replace-options-required", required(errorMessages.OPTION_REQUIRED));
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "vehicle-parts",
|
name: "vehicle-parts",
|
||||||
async beforeRouteEnter(to, from, next) {
|
async beforeRouteEnter(to, from, next) {
|
||||||
// Call APIs
|
// Call APIs
|
||||||
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
|
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
|
||||||
// Settle promises and get results
|
// Settle promises and get results
|
||||||
const promiseResultMap = [{
|
const promiseResultMap = [
|
||||||
resultKey: "cmsContent",
|
{
|
||||||
promise: cmsContentPromise,
|
resultKey: "cmsContent",
|
||||||
}, ];
|
promise: cmsContentPromise,
|
||||||
|
},
|
||||||
|
];
|
||||||
const resultMap = await settleAllPromises(promiseResultMap);
|
const resultMap = await settleAllPromises(promiseResultMap);
|
||||||
// Call the "next" function to complete the transition to this page.
|
// Call the "next" function to complete the transition to this page.
|
||||||
next((vm) => {
|
next((vm) => {
|
||||||
|
|
@ -89,121 +86,150 @@ export default {
|
||||||
|
|
||||||
// Glass Part Question dynamic component
|
// Glass Part Question dynamic component
|
||||||
Object.keys(vm.$refs)
|
Object.keys(vm.$refs)
|
||||||
.filter((r) => r.includes(vm.RefPrefix) && vm.$refs[r][0] !== undefined)
|
.filter((r) => r.includes(vm.RefPrefix) && vm.$refs[r][0] !== undefined)
|
||||||
.forEach((c) =>
|
.forEach((c) =>
|
||||||
vm.$refs[c][0].initializeComponent({
|
vm.$refs[c][0].initializeComponent({
|
||||||
ColorQuestionWidget: resultMap.cmsContent.ColorQuestionWidget,
|
ColorQuestionWidget: resultMap.cmsContent.ColorQuestionWidget,
|
||||||
FeatureQuestionWidget: resultMap.cmsContent.FeatureQuestionWidget,
|
FeatureQuestionWidget: resultMap.cmsContent.FeatureQuestionWidget,
|
||||||
})
|
})
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
glassParts: {},
|
|
||||||
alertWidgetData: Object,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
Form,
|
|
||||||
glassPartQuestion,
|
|
||||||
funnelHeader,
|
|
||||||
vehicleBanner,
|
|
||||||
funnelSubHeader,
|
|
||||||
funnelFooter,
|
|
||||||
alert,
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
PartsForQuestions() {
|
|
||||||
const partsData = this.PartsFromApi;
|
|
||||||
|
|
||||||
// Map API result data, to vehicle-parts data structure
|
|
||||||
const mappedData = partsData.partsOrQuestions.map((g) => {
|
|
||||||
return {
|
|
||||||
glassName: g.glassName,
|
|
||||||
glassLocation: g.glassLocation,
|
|
||||||
colorAnswers: g.parts.reduce((arr, p) => {
|
|
||||||
arr.push({
|
|
||||||
ColorAnswerText: p.color,
|
|
||||||
FeatureAnswers: [
|
|
||||||
{
|
|
||||||
FeatureAnswerText:
|
|
||||||
p.description === "" ? p.color : p.description,
|
|
||||||
PartNumber: p.partNumber,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
return arr;
|
|
||||||
}, []),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return mappedData;
|
|
||||||
},
|
|
||||||
|
|
||||||
PartsFromApi() {
|
|
||||||
return store.getters.pageData(fmgPageValues.VEHICLE_PARTS);
|
|
||||||
},
|
|
||||||
|
|
||||||
RefPrefix() {
|
|
||||||
return "partQuestion";
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
arePagePrerequisitesValid() {
|
|
||||||
// Check if isRepair is populated and if the pageData we need is here (Parts data)
|
|
||||||
if (
|
|
||||||
(store.getters.damage.isRepair != null) &&
|
|
||||||
Object.keys(store.getters.pageData(fmgPageValues.VEHICLE_PARTS))
|
|
||||||
.length !== 0
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
backButtonAction() {
|
|
||||||
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
|
|
||||||
},
|
|
||||||
async forwardButtonAction() {
|
|
||||||
const selectedGlassPartNumbers = [];
|
|
||||||
const matchedParts = [];
|
|
||||||
|
|
||||||
// Compile all selected parts from the page.
|
|
||||||
for (let [key, value] of Object.entries(this.glassParts)) {
|
|
||||||
for (let [glassKey, glassValue] of Object.entries(value)) {
|
|
||||||
selectedGlassPartNumbers.push(glassValue[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match them to the parts from the API.
|
|
||||||
for (let [key, value] of Object.entries(
|
|
||||||
this.PartsFromApi.partsOrQuestions
|
|
||||||
)) {
|
|
||||||
for (let [partKey, partValue] of Object.entries(value.parts)) {
|
|
||||||
const currentPart =
|
|
||||||
this.PartsFromApi.partsOrQuestions[key].parts[partKey];
|
|
||||||
const isMatched = selectedGlassPartNumbers.some(
|
|
||||||
(p) => p === currentPart.partNumber
|
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
glassParts: {},
|
||||||
|
alertWidgetData: Object,
|
||||||
|
alreadyPopulatedPartsData: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Form,
|
||||||
|
glassPartQuestion,
|
||||||
|
funnelHeader,
|
||||||
|
vehicleBanner,
|
||||||
|
funnelSubHeader,
|
||||||
|
funnelFooter,
|
||||||
|
alert,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isForwardActionDisabled() {
|
||||||
|
return (
|
||||||
|
Object.keys(this.matchedParts).length !==
|
||||||
|
this.PartsFromApi.partsOrQuestions.length
|
||||||
|
);
|
||||||
|
},
|
||||||
|
matchedParts() {
|
||||||
|
const matchedParts = [];
|
||||||
|
|
||||||
if (isMatched) {
|
// Match them to the parts from the API.
|
||||||
matchedParts.push(currentPart);
|
this.PartsFromApi.partsOrQuestions.forEach((part) => {
|
||||||
|
const selectedPartForGlassLocationAndName =
|
||||||
|
this.glassParts[`${part.glassLocation}-${part.glassName}`];
|
||||||
|
const selectedPartData = part.parts.filter(
|
||||||
|
(part) =>
|
||||||
|
selectedPartForGlassLocationAndName &&
|
||||||
|
part.partNumber == selectedPartForGlassLocationAndName?.partNumber
|
||||||
|
)[0];
|
||||||
|
|
||||||
|
if (selectedPartData) matchedParts.push(selectedPartData);
|
||||||
|
});
|
||||||
|
|
||||||
|
return matchedParts;
|
||||||
|
},
|
||||||
|
PartsOrQuestions() {
|
||||||
|
const partsData = this.PartsFromApi;
|
||||||
|
|
||||||
|
// Map API result data, to vehicle-parts data structure
|
||||||
|
const mappedData = partsData.partsOrQuestions.map((g) => {
|
||||||
|
return {
|
||||||
|
glassName: g.glassName,
|
||||||
|
glassLocation: g.glassLocation,
|
||||||
|
colorAnswers: g.parts.reduce((arr, p) => {
|
||||||
|
arr.push({
|
||||||
|
ColorAnswerText: p.color,
|
||||||
|
FeatureAnswers: [
|
||||||
|
{
|
||||||
|
FeatureAnswerText:
|
||||||
|
p.description === "" ? p.color : p.description,
|
||||||
|
PartNumber: p.partNumber,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return arr;
|
||||||
|
}, []),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return mappedData;
|
||||||
|
},
|
||||||
|
|
||||||
|
PartsFromApi() {
|
||||||
|
return store.getters.pageData(fmgPageValues.VEHICLE_PARTS);
|
||||||
|
},
|
||||||
|
|
||||||
|
RefPrefix() {
|
||||||
|
return "partQuestion";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
arePagePrerequisitesValid() {
|
||||||
|
// Check if isRepair is populated and if the pageData we need is here (Parts data)
|
||||||
|
if (
|
||||||
|
store.getters.damage.isRepair != null &&
|
||||||
|
Object.keys(store.getters.pageData(fmgPageValues.VEHICLE_PARTS))
|
||||||
|
.length !== 0
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
backButtonAction() {
|
||||||
|
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
|
||||||
|
},
|
||||||
|
async forwardButtonAction() {
|
||||||
|
const selectedGlassPartNumbers = [];
|
||||||
|
const matchedParts = [];
|
||||||
|
|
||||||
|
// Compile all selected parts from the page.
|
||||||
|
for (let [key, value] of Object.entries(this.glassParts)) {
|
||||||
|
for (let [glassKey, glassValue] of Object.entries(value)) {
|
||||||
|
selectedGlassPartNumbers.push(glassValue[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// If no parts could be matched, throw an error.
|
// Match them to the parts from the API.
|
||||||
if (matchedParts.length === 0) {
|
for (let [key, value] of Object.entries(this.PartsFromApi.partsOrQuestions)) {
|
||||||
throw new Error("Could not match any parts to the selected parts");
|
for (let [partKey, partValue] of Object.entries(value.parts)) {
|
||||||
}
|
const currentPart =
|
||||||
|
this.PartsFromApi.partsOrQuestions[key].parts[partKey];
|
||||||
|
const isMatched = selectedGlassPartNumbers.some(
|
||||||
|
(p) => p === currentPart.partNumber
|
||||||
|
);
|
||||||
|
|
||||||
// Save parts to the store.
|
if (isMatched) {
|
||||||
await this.dispatchStoreAction(storeActions.SAVE_GLASS_PARTS, matchedParts);
|
matchedParts.push(currentPart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no parts could be matched, throw an error.
|
||||||
|
if (this.isForwardActionDisabled) {
|
||||||
|
this.$refs.funnelFooter.removeLoader();
|
||||||
|
throw new Error("Could not match any parts to the selected parts");
|
||||||
|
}
|
||||||
|
|
||||||
// Navigate to the next page.
|
// Save parts to the store.
|
||||||
this.$router.navigate(this.navigationScenarios.SELECTED_PARTS,this.$route);
|
await this.dispatchStoreAction(
|
||||||
|
storeActions.SAVE_GLASS_PARTS,
|
||||||
|
matchedParts
|
||||||
|
);
|
||||||
|
|
||||||
|
// Navigate to the next page.
|
||||||
|
this.$router.navigate(
|
||||||
|
this.navigationScenarios.SELECTED_PARTS,
|
||||||
|
this.$route
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
LoadInitialPartsData() {
|
LoadInitialPartsData() {
|
||||||
|
|
@ -222,14 +248,14 @@ methods: {
|
||||||
this.glassParts[g.glassLocation + "-" + g.glassName] = {
|
this.glassParts[g.glassLocation + "-" + g.glassName] = {
|
||||||
[g.glassLocation]: [partNumber],
|
[g.glassLocation]: [partNumber],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.LoadInitialPartsData();
|
||||||
},
|
},
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.LoadInitialPartsData();
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
:answers="styles"
|
:answers="styles"
|
||||||
groupName="ChooseVehicleStyle"
|
groupName="ChooseVehicleStyle"
|
||||||
textPosition="text-start"
|
textPosition="text-start"
|
||||||
v-model="selectedValueAsArray"
|
v-model="selectedValue"
|
||||||
isRequired=true
|
isRequired=true
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -34,14 +34,13 @@ export default {
|
||||||
questionText(){
|
questionText(){
|
||||||
return this.getCmsContent(this.cmsWidgetName, 'QuestionText');
|
return this.getCmsContent(this.cmsWidgetName, 'QuestionText');
|
||||||
},
|
},
|
||||||
selectedValueAsArray: {
|
selectedValue: {
|
||||||
get: function() {
|
get: function() {
|
||||||
const modelValueAsArray = this.modelValue ? [this.modelValue] : [];
|
const modelValueAsArray = this.modelValue ? [this.modelValue] : [];
|
||||||
return modelValueAsArray;
|
return modelValueAsArray;
|
||||||
},
|
},
|
||||||
set: function(newValue) {
|
set: function(newValue) {
|
||||||
const newValueAsScalar = newValue && newValue.length > 0 ? newValue[0] : null;
|
this.$emit("update:modelValue", newValue);
|
||||||
this.$emit("update:modelValue", newValueAsScalar);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
:answers="years"
|
:answers="years"
|
||||||
groupName="ChooseVehicleYear"
|
groupName="ChooseVehicleYear"
|
||||||
textPosition="text-start"
|
textPosition="text-start"
|
||||||
v-model="selectedValueAsArray"
|
v-model="selectedValue"
|
||||||
isRequired=true
|
isRequired=true
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -35,14 +35,13 @@ export default {
|
||||||
questionText(){
|
questionText(){
|
||||||
return this.getCmsContent(this.cmsWidgetName, 'QuestionText');
|
return this.getCmsContent(this.cmsWidgetName, 'QuestionText');
|
||||||
},
|
},
|
||||||
selectedValueAsArray: {
|
selectedValue: {
|
||||||
get: function() {
|
get: function() {
|
||||||
const modelValueAsArray = this.modelValue ? [this.modelValue] : [];
|
const modelValueAsArray = this.modelValue ? [this.modelValue] : [];
|
||||||
return modelValueAsArray;
|
return modelValueAsArray;
|
||||||
},
|
},
|
||||||
set: function(newValue) {
|
set: function(newValue) {
|
||||||
const newValueAsScalar = newValue && newValue.length > 0 ? newValue[0] : null;
|
this.$emit("update:modelValue", newValue);
|
||||||
this.$emit("update:modelValue", newValueAsScalar);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -177,15 +177,10 @@ export default {
|
||||||
previouslyEnteredCarId: '',
|
previouslyEnteredCarId: '',
|
||||||
invalidZip: '',
|
invalidZip: '',
|
||||||
vinPopulatedOnPageLoad: this.getVinFromStore()?.length > 0,
|
vinPopulatedOnPageLoad: this.getVinFromStore()?.length > 0,
|
||||||
isInsuranceVerified: false,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.attachCustomEvents();
|
this.attachCustomEvents();
|
||||||
if (this.vinPopulatedOnPageLoad) {
|
|
||||||
this.setupVinMask();
|
|
||||||
this.isInsuranceVerified = store.getters.payment.insuranceCoverage.isVerified || getFunnelCookie().HasDelayedClaimRegistration;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
vin() {
|
vin() {
|
||||||
|
|
@ -231,12 +226,20 @@ export default {
|
||||||
return this.getCmsContent("PerfectMatchInsuranceVerifiedAlert", "BodyText").replaceAll("{custom:damage}",
|
return this.getCmsContent("PerfectMatchInsuranceVerifiedAlert", "BodyText").replaceAll("{custom:damage}",
|
||||||
getIsWindshieldOnly())
|
getIsWindshieldOnly())
|
||||||
},
|
},
|
||||||
|
isInsuranceVerified() {
|
||||||
|
return store.getters.payment.insuranceCoverage.isVerified || getFunnelCookie().HasDelayedClaimRegistration;
|
||||||
|
},
|
||||||
|
vinMask() {
|
||||||
|
if (this.vinPopulatedOnPageLoad) {
|
||||||
|
const lastSixChars = this.vin.substring(11, this.vin.length);
|
||||||
|
return `!X!X!X!X!X!X!X!X!X!X!X${lastSixChars}`;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return 'XXXXXXXXXXXXXXXXX';
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setupVinMask() {
|
|
||||||
const lastSixChars = this.vin.substring(11, this.vin.length);
|
|
||||||
this.vinMask = `!X!X!X!X!X!X!X!X!X!X!X${lastSixChars}`;
|
|
||||||
},
|
|
||||||
arePagePrerequisitesValid() {
|
arePagePrerequisitesValid() {
|
||||||
return store.getters.vehicle.carId !== null;
|
return store.getters.vehicle.carId !== null;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ const fmgPageValues = {
|
||||||
REVEAL: "reveal",
|
REVEAL: "reveal",
|
||||||
ESTIMATE: "estimate",
|
ESTIMATE: "estimate",
|
||||||
ADDRESS_VEHICLES: "address-vehicles",
|
ADDRESS_VEHICLES: "address-vehicles",
|
||||||
|
QUOTE: "quote"
|
||||||
};
|
};
|
||||||
|
|
||||||
export { fmgPageValues };
|
export { fmgPageValues };
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ html {
|
||||||
input[type=radio]+label:before,
|
input[type=radio]+label:before,
|
||||||
input[type=checkbox]+label:before {
|
input[type=checkbox]+label:before {
|
||||||
border: 1px solid $red;
|
border: 1px solid $red;
|
||||||
|
background-color: initial;
|
||||||
}
|
}
|
||||||
input[type=checkbox]:checked + label:before {
|
input[type=checkbox]:checked + label:before {
|
||||||
border: 1px solid $blue;
|
border: 1px solid $blue;
|
||||||
|
|
|
||||||
|
|
@ -224,17 +224,25 @@ export default {
|
||||||
font-size: .875rem;
|
font-size: .875rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.col {
|
||||||
&:first-of-type {
|
&:first-of-type {
|
||||||
label {
|
.list-button-horizontal {
|
||||||
border-bottom-left-radius: 0.5rem;
|
label {
|
||||||
border-top-left-radius: 0.5rem;
|
border-bottom-left-radius: 0.5rem;
|
||||||
z-index: 2;
|
border-top-left-radius: 0.5rem;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-of-type {
|
&:last-of-type {
|
||||||
label {
|
.list-button-horizontal {
|
||||||
border-bottom-right-radius: 0.5rem;
|
label {
|
||||||
border-top-right-radius: 0.5rem;
|
border-bottom-right-radius: 0.5rem;
|
||||||
|
border-top-right-radius: 0.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,7 @@ describe("list-card.vue", () => {
|
||||||
});
|
});
|
||||||
wrapper.vm.handleCheckChange();
|
wrapper.vm.handleCheckChange();
|
||||||
// Assert
|
// Assert
|
||||||
expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{value: "List Card Checkbox", checkValue: [Boolean, String], buttonId: 'list-card-id'}]);
|
expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{value: "List Card Checkbox", checkValue: "List Card Checkbox", buttonId: 'list-card-id'}]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should set checkValue data if selectedButtonIDs has value(s)", async () => {
|
it("Should set checkValue data if selectedButtonIDs has value(s)", async () => {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="'col' + colLength">
|
<div :class="{'h-100': !isWide}">
|
||||||
<div
|
<div
|
||||||
class="list-card w-100 rounded-3 d-flex align-items-center h-100"
|
class="list-card w-100 rounded-3 d-flex align-items-center"
|
||||||
:class="[
|
:class="[
|
||||||
|
'h-100',
|
||||||
isWide ? 'horizontal' : '',
|
isWide ? 'horizontal' : '',
|
||||||
(errors.length > 0 || hasError) ? 'has-error' : '',
|
(errors.length > 0 || hasError) ? 'has-error' : '',
|
||||||
]"
|
]"
|
||||||
|
|
@ -19,7 +20,8 @@
|
||||||
:value="value"
|
:value="value"
|
||||||
:aria-required="isRequired"
|
:aria-required="isRequired"
|
||||||
v-model="checkValue"
|
v-model="checkValue"
|
||||||
@change="handleInputChange()"
|
:checked="checkValue"
|
||||||
|
@change="handleInputChange"
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
|
|
@ -27,7 +29,7 @@
|
||||||
:aria-labelledby="buttonID"
|
:aria-labelledby="buttonID"
|
||||||
class="d-flex w-100 align-items-center px-2 h-100"
|
class="d-flex w-100 align-items-center px-2 h-100"
|
||||||
:class="getLabelClasses"
|
:class="getLabelClasses"
|
||||||
@mouseup="triggerButton()"
|
@mouseup="triggerButton"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
:id="buttonImageId"
|
:id="buttonImageId"
|
||||||
|
|
@ -93,7 +95,7 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
checkValue: [Boolean, String],
|
checkValue: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
|
@ -102,6 +104,14 @@ export default {
|
||||||
? this.selectedValues.includes(this.value)
|
? this.selectedValues.includes(this.value)
|
||||||
: this.selectedValues[0];
|
: this.selectedValues[0];
|
||||||
}
|
}
|
||||||
|
else if (Array.isArray(this.modelValue)) {
|
||||||
|
this.checkValue = this.isMultiSelect
|
||||||
|
? this.modelValue.includes(this.value)
|
||||||
|
: this.modelValue[0];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.checkValue = this.selectedValues == this.value || this.modelValue == this.value;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
unmounted() { // needed to clear this button's selectedValues if it is removed to keep validation in sync
|
unmounted() { // needed to clear this button's selectedValues if it is removed to keep validation in sync
|
||||||
if (this.clearOnUnmount) {
|
if (this.clearOnUnmount) {
|
||||||
|
|
@ -164,6 +174,15 @@ export default {
|
||||||
this.checkValue = newVal.value;
|
this.checkValue = newVal.value;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
selectedValues(newVal) {
|
||||||
|
if (typeof newVal === "string") {
|
||||||
|
this.handleChange(this.value);
|
||||||
|
this.checkValue = newVal == this.value;
|
||||||
|
}
|
||||||
|
else if (newVal !== undefined) {
|
||||||
|
this.checkValue = newVal.value;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const inputType = props.isMultiSelect ? "checkbox" : "radio";
|
const inputType = props.isMultiSelect ? "checkbox" : "radio";
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ describe("radio.vue", () => {
|
||||||
isRequired: true,
|
isRequired: true,
|
||||||
modelValue: ["List Card Checkbox"],
|
modelValue: ["List Card Checkbox"],
|
||||||
value: "Car-Front",
|
value: "Car-Front",
|
||||||
selectedValues: ["Car-Front"]
|
selectedValues: "Car-Front"
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// Assert
|
// Assert
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
<template>
|
<template>
|
||||||
<!-- Checkbox groups MUST be wrapped in a <fieldset> and <legend> tag and must contain tabindex -->
|
<!-- Checkbox groups MUST be wrapped in a <fieldset> and <legend> tag and must contain tabindex -->
|
||||||
<div class="ui-radio form-check" :class="[hasError ? 'has-error' : '']">
|
<div class="ui-radio form-check" :class="[(errors.length > 0 || hasError) ? 'has-error' : '']">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
aria-checked="false"
|
aria-checked="false"
|
||||||
:name="groupName"
|
:name="groupName"
|
||||||
:id="buttonID"
|
:id="buttonID"
|
||||||
:aria-required="isRequired"
|
:aria-required="isRequired"
|
||||||
:value="value"
|
:value="value"
|
||||||
:v-model="checkValue"
|
:v-model="checkValue"
|
||||||
@change="handleCheckChange()"
|
@change="handleCheckChange"
|
||||||
:checked="checkValue"
|
:checked="checkValue"
|
||||||
/>
|
:validationRules="validationRules"
|
||||||
|
/>
|
||||||
<label class="d-flex align-items-start form-check-label" :for="buttonID">
|
<label class="d-flex align-items-start form-check-label" :for="buttonID">
|
||||||
<p v-if="buttonLabel" class="m-0">{{ buttonLabel }}</p>
|
<p v-if="buttonLabel" class="m-0">{{ buttonLabel }}</p>
|
||||||
<span v-if="screenReaderOnlyText" class="sr-only">{{
|
<span v-if="screenReaderOnlyText" class="sr-only">{{
|
||||||
|
|
@ -37,8 +38,9 @@ export default {
|
||||||
default: "",
|
default: "",
|
||||||
},
|
},
|
||||||
screenReaderOnlyText: String,
|
screenReaderOnlyText: String,
|
||||||
selectedValues: [Array, String],
|
selectedValues: String,
|
||||||
hasError: Boolean,
|
hasError: Boolean,
|
||||||
|
validationRules: String
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -47,16 +49,15 @@ export default {
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
if (this.selectedValues) {
|
if (this.selectedValues) {
|
||||||
this.checkValue = this.selectedValues[0] === this.value;
|
this.checkValue = this.selectedValues === this.value;
|
||||||
}else{
|
this.handleCheckChange();
|
||||||
|
} else{
|
||||||
this.checkValue = false;
|
this.checkValue = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleClick(value) {
|
|
||||||
this.handleChange(value);
|
|
||||||
},
|
|
||||||
handleCheckChange() {
|
handleCheckChange() {
|
||||||
|
this.handleChange(this.value);
|
||||||
const emitEvent = {
|
const emitEvent = {
|
||||||
checkValue: this.checkValue,
|
checkValue: this.checkValue,
|
||||||
value: this.value.toString(),
|
value: this.value.toString(),
|
||||||
|
|
@ -88,13 +89,17 @@ export default {
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.form-check {
|
.form-check {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
.form-check-input {
|
.form-check-input {
|
||||||
border: 1px solid $gray-500;
|
border: 1px solid $gray-500;
|
||||||
border-radius: 100%;
|
border-radius: 50%;
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
|
|
||||||
&:checked {
|
&:checked {
|
||||||
background-color: $white;
|
background-color: $white;
|
||||||
background-size: 71%;
|
background-size: 71%;
|
||||||
|
background-position: center;
|
||||||
border: 1px solid $blue;
|
border: 1px solid $blue;
|
||||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 50 50' xml:space='preserve'%3e%3ccircle cx='25' cy='25' r='25' fill='%231574A1'/%3e%3c/svg%3e");
|
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 50 50' xml:space='preserve'%3e%3ccircle cx='25' cy='25' r='25' fill='%231574A1'/%3e%3c/svg%3e");
|
||||||
+ label {
|
+ label {
|
||||||
|
|
@ -108,7 +113,15 @@ export default {
|
||||||
&:focus {
|
&:focus {
|
||||||
box-shadow: 0 0 0 2.5px $blue;
|
box-shadow: 0 0 0 2.5px $blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&, & + label {
|
||||||
|
margin-top: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
.form-check-input {
|
.form-check-input {
|
||||||
box-shadow: 0 0 0 4px $blue-300;
|
box-shadow: 0 0 0 4px $blue-300;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue