Merge branch 'develop' into feature/CSR-108
This commit is contained in:
commit
56c26077b5
23 changed files with 593 additions and 496 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ const maska = jest.fn();
|
||||||
describe("textboxQuestion.vue", () => {
|
describe("textboxQuestion.vue", () => {
|
||||||
|
|
||||||
it("Should render a text input", async () => {
|
it("Should render a text input", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const wrapper = shallowMount(textboxQuestion, {
|
const wrapper = shallowMount(textboxQuestion, {
|
||||||
global: {
|
global: {
|
||||||
directives: {
|
directives: {
|
||||||
|
|
@ -53,35 +53,7 @@ describe("textboxQuestion.vue", () => {
|
||||||
expect(label.text()).toContain(questionText);
|
expect(label.text()).toContain(questionText);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should render the 'questionText' data value with '⁠' after the first character of each word in the label text when disableAutoFill is true.", async () => {
|
|
||||||
// Arrange
|
|
||||||
const wrapper = shallowMount(textboxQuestion, {
|
|
||||||
global: {
|
|
||||||
directives: {
|
|
||||||
maska: maska,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
propsData: {
|
|
||||||
disableAutoFill: true,
|
|
||||||
},
|
|
||||||
mixins: [mockMixin]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mock CMS content ...
|
|
||||||
// Trust me, the below instance of the string "Question Text" actually has the ⁠ in it. You just can't see it
|
|
||||||
// Don't believe me? Copy and paste it into Google. Then inspect the search field element in Dev Tools,
|
|
||||||
// you will see "Q⁠uestion T⁠ext"
|
|
||||||
const expectedQuestionText = "Question Text";
|
|
||||||
|
|
||||||
// Act
|
|
||||||
const label = wrapper.find("label");
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(label.text()).toContain(expectedQuestionText);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return input id as the id of the input field", async () => {
|
it("Should return input id as the id of the input field", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const wrapper = shallowMount(textboxQuestion, {
|
const wrapper = shallowMount(textboxQuestion, {
|
||||||
|
|
@ -103,7 +75,7 @@ describe("textboxQuestion.vue", () => {
|
||||||
expect(input.attributes().id).toEqual("input ID");
|
expect(input.attributes().id).toEqual("input ID");
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should render the 'questionText' data value as the aria-label attribute.", async () => {
|
it("Should render the 'questionText' data value as the aria-label attribute.", async () => {
|
||||||
|
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
@ -193,6 +165,6 @@ describe("textboxQuestion.vue", () => {
|
||||||
// Assert
|
// Assert
|
||||||
expect(wrapper.vm.handleChange).toHaveBeenCalled;
|
expect(wrapper.vm.handleChange).toHaveBeenCalled;
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="textbox-question" :class="(errors && errors.length) || hasError ? 'has-error' : ''">
|
<div class="textbox-question" :class="(errors && errors.length) || hasError ? 'has-error' : ''">
|
||||||
<label :for="inputId" :aria-label="questionText" class="form-label" v-html="labelText"></label>
|
<label :for="inputId" :aria-label="questionText" class="form-label" v-html="labelText"></label>
|
||||||
<!-- See https://stackoverflow.com/a/30976223 for information about "do-not-autofill" -->
|
|
||||||
<input
|
<input
|
||||||
v-model.trim="value"
|
v-model.trim="value"
|
||||||
v-maska="mask"
|
v-maska="mask"
|
||||||
|
|
@ -14,7 +13,6 @@
|
||||||
:aria-disabled="isDisabled"
|
:aria-disabled="isDisabled"
|
||||||
:disabled="isDisabled"
|
:disabled="isDisabled"
|
||||||
:aria-required="isRequired"
|
:aria-required="isRequired"
|
||||||
autocomplete="do-not-autofill"
|
|
||||||
:class="[hasIcon ? 'has-icon' : '', iconRight ? 'icon-right' : '']"
|
:class="[hasIcon ? 'has-icon' : '', iconRight ? 'icon-right' : '']"
|
||||||
:validationRules="validationRules"
|
:validationRules="validationRules"
|
||||||
@change="handleChange"
|
@change="handleChange"
|
||||||
|
|
@ -46,7 +44,6 @@ export default {
|
||||||
inputId: String,
|
inputId: String,
|
||||||
isDisabled: Boolean,
|
isDisabled: Boolean,
|
||||||
isRequired: Boolean,
|
isRequired: Boolean,
|
||||||
disableAutoFill: Boolean,
|
|
||||||
hasIcon: Boolean, // If input has an icon
|
hasIcon: Boolean, // If input has an icon
|
||||||
iconRight: Boolean, // Place icon on right side of text input, otherwise default is left if hasIcon prop is used
|
iconRight: Boolean, // Place icon on right side of text input, otherwise default is left if hasIcon prop is used
|
||||||
hasError: Boolean,
|
hasError: Boolean,
|
||||||
|
|
@ -132,7 +129,7 @@ export default {
|
||||||
const result = await validate(newValue, this.validationRules); // do a test validation check, without triggering full validation
|
const result = await validate(newValue, this.validationRules); // do a test validation check, without triggering full validation
|
||||||
if (result.valid) {
|
if (result.valid) {
|
||||||
this.handleChange(newValue); // trigger full validation on this field only
|
this.handleChange(newValue); // trigger full validation on this field only
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
<loadingModal ref="loadingModal"/>
|
<loadingModal ref="loadingModal"/>
|
||||||
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
|
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
|
||||||
<vehicleBanner cmsWidgetName="VehicleBannerWidget" ref="vehicleBanner" :displayGenericVehicleImage=false />
|
<vehicleBanner cmsWidgetName="VehicleBannerWidget" ref="vehicleBanner" :displayGenericVehicleImage=false />
|
||||||
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" ref="funnelSubHeader" />
|
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" ref="funnelSubHeader" />
|
||||||
<div class="fade-on-route-transition sub-container make-tall">
|
<div class="fade-on-route-transition sub-container make-tall">
|
||||||
<customerQuestions ref="customerQuestions" v-model="customerQuestions" />
|
<customerQuestions ref="customerQuestions" v-model="customerQuestions" />
|
||||||
<alert ref="alertVinNotFound" v-if="displayVinNotFoundAlert"
|
<alert ref="alertVinNotFound" v-if="displayVinNotFoundAlert"
|
||||||
|
|
@ -24,14 +24,14 @@
|
||||||
:manualCopy="AlertMatchedDifferentVehicleBody"
|
:manualCopy="AlertMatchedDifferentVehicleBody"
|
||||||
alertClass="alert-warning"
|
alertClass="alert-warning"
|
||||||
v-bind:isDismissible="false"
|
v-bind:isDismissible="false"
|
||||||
/>
|
/>
|
||||||
<alert ref="alertNonServiceableZip" v-if="displayNonServiceableZipAlert"
|
<alert ref="alertNonServiceableZip" v-if="displayNonServiceableZipAlert"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
alertClass="alert-danger"
|
alertClass="alert-danger"
|
||||||
:manualHeadline="AlertNonServiceableZipHeader"
|
:manualHeadline="AlertNonServiceableZipHeader"
|
||||||
:manualCopy="AlertNonServiceableZipBody"
|
:manualCopy="AlertNonServiceableZipBody"
|
||||||
v-bind:isDismissible="false"
|
v-bind:isDismissible="false"
|
||||||
/>
|
/>
|
||||||
<alert ref="alertVinLookupsByHomeAddressNotAllowed" v-if="displayVinLookupByHomeAddressNotAllowedAlert"
|
<alert ref="alertVinLookupsByHomeAddressNotAllowed" v-if="displayVinLookupByHomeAddressNotAllowedAlert"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
cmsWidgetName="AlertVinLookupsByHomeAddressNotAllowedWidget"
|
cmsWidgetName="AlertVinLookupsByHomeAddressNotAllowedWidget"
|
||||||
|
|
@ -42,11 +42,11 @@
|
||||||
<div class="service-zip-field" v-if="showServiceZipField" aria-live="polite">
|
<div class="service-zip-field" v-if="showServiceZipField" aria-live="polite">
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<textboxQuestion cmsWidgetName="ServiceZipQuestionWidget" v-model="serviceZipCode" ref="serviceZip" inputId="7add1b26df344f2caf1678de5797803f" aria-haspopup="" mask="#####" disableAutoFill validationRules="service-zip-required|service-zip-format" />
|
<textboxQuestion cmsWidgetName="ServiceZipQuestionWidget" v-model="serviceZipCode" ref="serviceZip" inputId="7add1b26df344f2caf1678de5797803f" aria-haspopup="" mask="#####" validationRules="service-zip-required|service-zip-format" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
<funnel-footer
|
<funnel-footer
|
||||||
cmsWidgetName="FunnelFooterWidget"
|
cmsWidgetName="FunnelFooterWidget"
|
||||||
ref="funnelFooter"
|
ref="funnelFooter"
|
||||||
|
|
@ -55,7 +55,7 @@
|
||||||
@back-clicked="backButtonAction"
|
@back-clicked="backButtonAction"
|
||||||
:isForwardActionDisabled="!meta.valid"
|
:isForwardActionDisabled="!meta.valid"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -122,14 +122,14 @@ export default {
|
||||||
firstName: this.getRegistrationFirstNameFromStore(),
|
firstName: this.getRegistrationFirstNameFromStore(),
|
||||||
lastName: this.getRegistrationLastNameFromStore(),
|
lastName: this.getRegistrationLastNameFromStore(),
|
||||||
emailAddress: this.getEmailFromStore(),
|
emailAddress: this.getEmailFromStore(),
|
||||||
},
|
},
|
||||||
serviceZipCode: this.getServiceZipFromStore(),
|
serviceZipCode: this.getServiceZipFromStore(),
|
||||||
displayNonServiceableZipAlert: false,
|
displayNonServiceableZipAlert: false,
|
||||||
displayVinNotFoundAlert: false,
|
displayVinNotFoundAlert: false,
|
||||||
displayMatchedDifferentVehicleAlert: false,
|
displayMatchedDifferentVehicleAlert: false,
|
||||||
displayVinLookupByHomeAddressNotAllowedAlert: false,
|
displayVinLookupByHomeAddressNotAllowedAlert: false,
|
||||||
previouslyEnteredCarId: "",
|
previouslyEnteredCarId: "",
|
||||||
isSelectedGlassAvailableForVehicle: false,
|
isSelectedGlassAvailableForVehicle: false,
|
||||||
customAlertData: {},
|
customAlertData: {},
|
||||||
showServiceZipField: this.getServiceZipFromStore(),
|
showServiceZipField: this.getServiceZipFromStore(),
|
||||||
isZipServicable: false,
|
isZipServicable: false,
|
||||||
|
|
@ -150,7 +150,7 @@ export default {
|
||||||
this.$route
|
this.$route
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
attachCustomEvents() {
|
attachCustomEvents() {
|
||||||
this.prependActionToMethod(this, this.forwardButtonAction, () => {
|
this.prependActionToMethod(this, this.forwardButtonAction, () => {
|
||||||
this.pushEventToGA(
|
this.pushEventToGA(
|
||||||
this.$route.query[this.queryStrings.FMG_PAGE],
|
this.$route.query[this.queryStrings.FMG_PAGE],
|
||||||
|
|
@ -177,7 +177,7 @@ export default {
|
||||||
},
|
},
|
||||||
getRegistrationLastNameFromStore() {
|
getRegistrationLastNameFromStore() {
|
||||||
return store.getters.vehicle.registration.lastName;
|
return store.getters.vehicle.registration.lastName;
|
||||||
},
|
},
|
||||||
getEmailFromStore() {
|
getEmailFromStore() {
|
||||||
return store.getters.order.customer.emailAddress;
|
return store.getters.order.customer.emailAddress;
|
||||||
},
|
},
|
||||||
|
|
@ -189,9 +189,9 @@ export default {
|
||||||
|
|
||||||
// Lookup VIN(s) with the provided address
|
// Lookup VIN(s) with the provided address
|
||||||
const vinLookupPromise = this.lookupVin(
|
const vinLookupPromise = this.lookupVin(
|
||||||
this.customerQuestions.lastName,
|
this.customerQuestions.lastName,
|
||||||
this.customerQuestions.addressQuestions.streetAddress,
|
this.customerQuestions.addressQuestions.streetAddress,
|
||||||
this.customerQuestions.addressQuestions.zipCode,
|
this.customerQuestions.addressQuestions.zipCode,
|
||||||
this.customerQuestions.addressQuestions.state
|
this.customerQuestions.addressQuestions.state
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -200,8 +200,8 @@ export default {
|
||||||
|
|
||||||
const vinLookupResponse = await vinLookupPromise;
|
const vinLookupResponse = await vinLookupPromise;
|
||||||
const serviceZipValidationResponse = await serviceZipValidationPromise;
|
const serviceZipValidationResponse = await serviceZipValidationPromise;
|
||||||
|
|
||||||
if (!vinLookupResponse.data.isStatePermissible) {
|
if (!vinLookupResponse.data.isStatePermissible) {
|
||||||
// State Restrictions forbid lookup by address
|
// State Restrictions forbid lookup by address
|
||||||
this.displayVinLookupByHomeAddressNotAllowedAlert = true;
|
this.displayVinLookupByHomeAddressNotAllowedAlert = true;
|
||||||
this.$refs.funnelFooter.removeLoader();
|
this.$refs.funnelFooter.removeLoader();
|
||||||
|
|
@ -210,29 +210,29 @@ export default {
|
||||||
|
|
||||||
// if the neither the registration zip code or service zip code are not serviceable
|
// if the neither the registration zip code or service zip code are not serviceable
|
||||||
this.isZipServicable = serviceZipValidationResponse.data.isServiceable;
|
this.isZipServicable = serviceZipValidationResponse.data.isServiceable;
|
||||||
if (!this.isZipServicable) {
|
if (!this.isZipServicable) {
|
||||||
this.displayNonServiceableZipAlert = true;
|
this.displayNonServiceableZipAlert = true;
|
||||||
this.showServiceZipField = true;
|
this.showServiceZipField = true;
|
||||||
this.$refs.funnelFooter.removeLoader();
|
this.$refs.funnelFooter.removeLoader();
|
||||||
} else if (!this.serviceZipCode) {
|
} else if (!this.serviceZipCode) {
|
||||||
// if the registration zip code is servicable and nothing was entered for the service zip code
|
// if the registration zip code is servicable and nothing was entered for the service zip code
|
||||||
// then set the service zip code to the registration zip code
|
// then set the service zip code to the registration zip code
|
||||||
this.serviceZipCode = this.customerQuestions.addressQuestions.zipCode;
|
this.serviceZipCode = this.customerQuestions.addressQuestions.zipCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const carEntered = store.getters.vehicle;
|
const carEntered = store.getters.vehicle;
|
||||||
const carsFound = vinLookupResponse.data.vinVehicles;
|
const carsFound = vinLookupResponse.data.vinVehicles;
|
||||||
|
|
||||||
if (carsFound.length == 0) {
|
if (carsFound.length == 0) {
|
||||||
// No VINs found
|
// No VINs found
|
||||||
this.displayVinNotFoundAlert = true;
|
this.displayVinNotFoundAlert = true;
|
||||||
this.$refs.funnelFooter.removeLoader();
|
this.$refs.funnelFooter.removeLoader();
|
||||||
return;
|
return;
|
||||||
} else if (carsFound.length == 1) {
|
} else if (carsFound.length == 1) {
|
||||||
const carFound = carsFound[0].vehicle;
|
const carFound = carsFound[0].vehicle;
|
||||||
this.isCarIdDifferent = carFound.carId !== carEntered.carId;
|
this.isCarIdDifferent = carFound.carId !== carEntered.carId;
|
||||||
|
|
||||||
if (this.isCarIdDifferent && carFound.carId !== this.previouslyEnteredCarId) {
|
if (this.isCarIdDifferent && carFound.carId !== this.previouslyEnteredCarId) {
|
||||||
// Display Alert
|
// Display Alert
|
||||||
this.previouslyEnteredCarId = carFound.carId;
|
this.previouslyEnteredCarId = carFound.carId;
|
||||||
this.customAlertData.vehicleInfo = carFound;
|
this.customAlertData.vehicleInfo = carFound;
|
||||||
|
|
@ -241,12 +241,12 @@ export default {
|
||||||
this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId(carFound.carId);
|
this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId(carFound.carId);
|
||||||
|
|
||||||
// Update button "Continue with..."
|
// Update button "Continue with..."
|
||||||
this.$refs.funnelFooter.updateButtonText(`Continue with ${carFound.year} ${carFound.make} ${carFound.model}`);
|
this.$refs.funnelFooter.updateButtonText(`Continue with ${carFound.year} ${carFound.make} ${carFound.model}`);
|
||||||
this.$refs.funnelFooter.removeLoader();
|
this.$refs.funnelFooter.removeLoader();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.isZipServicable) {
|
if (!this.isZipServicable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -254,21 +254,21 @@ export default {
|
||||||
// update data if the zip or service zip is servicable
|
// update data if the zip or service zip is servicable
|
||||||
this.updateVehicleInfo(carsFound[0].vin, carFound);
|
this.updateVehicleInfo(carsFound[0].vin, carFound);
|
||||||
this.updateCustomerInfo(serviceZipValidationResponse.data.state);
|
this.updateCustomerInfo(serviceZipValidationResponse.data.state);
|
||||||
|
|
||||||
} else if (carsFound.length > 1) {
|
} else if (carsFound.length > 1) {
|
||||||
if (!this.isZipServicable) {
|
if (!this.isZipServicable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if multiple cars were found
|
// if multiple cars were found
|
||||||
let matchingCars = carsFound.filter(car => car.vehicle.carId === carEntered.carId);
|
let matchingCars = carsFound.filter(car => car.vehicle.carId === carEntered.carId);
|
||||||
if (matchingCars.length === 1) {
|
if (matchingCars.length === 1) {
|
||||||
// and one and only one of them matches the carId entered, save the vehicle info
|
// and one and only one of them matches the carId entered, save the vehicle info
|
||||||
// so we can go to the Heritage Funnel directly
|
// so we can go to the Heritage Funnel directly
|
||||||
const matchingCar = matchingCars[0];
|
const matchingCar = matchingCars[0];
|
||||||
this.updateVehicleInfo(matchingCar.vin, matchingCar.vehicle);
|
this.updateVehicleInfo(matchingCar.vin, matchingCar.vehicle);
|
||||||
}
|
}
|
||||||
|
|
||||||
// update data if the zip or service zip is servicable
|
// update data if the zip or service zip is servicable
|
||||||
this.updateCustomerInfo(serviceZipValidationResponse.data.state);
|
this.updateCustomerInfo(serviceZipValidationResponse.data.state);
|
||||||
}
|
}
|
||||||
|
|
@ -277,13 +277,13 @@ export default {
|
||||||
},
|
},
|
||||||
resetWarningsAndErrors() {
|
resetWarningsAndErrors() {
|
||||||
this.displayVinNotFoundAlert = false;
|
this.displayVinNotFoundAlert = false;
|
||||||
this.displayNonServiceableZipAlert = false;
|
this.displayNonServiceableZipAlert = false;
|
||||||
this.displayMatchedDifferentVehicleAlert = false;
|
this.displayMatchedDifferentVehicleAlert = false;
|
||||||
this.displayVinLookupByHomeAddressNotAllowedAlert = false;
|
this.displayVinLookupByHomeAddressNotAllowedAlert = false;
|
||||||
},
|
},
|
||||||
navigateForward(carEntered, carsFound) {
|
navigateForward(carEntered, carsFound) {
|
||||||
this.updateServiceLocationIfNecessary();
|
this.updateServiceLocationIfNecessary();
|
||||||
|
|
||||||
if (carsFound.length == 1) {
|
if (carsFound.length == 1) {
|
||||||
// if a different vehicle is found than the one entered and the selected glass
|
// if a different vehicle is found than the one entered and the selected glass
|
||||||
// is not available for that vehicle
|
// is not available for that vehicle
|
||||||
|
|
@ -297,37 +297,37 @@ export default {
|
||||||
}, {}
|
}, {}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// otherwise
|
// otherwise
|
||||||
this.navigateForwardWithSingleCarMatch();
|
this.navigateForwardWithSingleCarMatch();
|
||||||
}
|
}
|
||||||
} else if (carsFound.length > 1) {
|
} else if (carsFound.length > 1) {
|
||||||
// if multiple cars were found
|
// if multiple cars were found
|
||||||
let matchingCars = carsFound.filter(car => car.vehicle.carId === carEntered.carId);
|
let matchingCars = carsFound.filter(car => car.vehicle.carId === carEntered.carId);
|
||||||
if (matchingCars.length === 1) {
|
if (matchingCars.length === 1) {
|
||||||
// and one and only of them matches the car id entered
|
// and one and only of them matches the car id entered
|
||||||
const matchingCar = matchingCars[0];
|
const matchingCar = matchingCars[0];
|
||||||
this.updateVehicleInfo(matchingCar.vin, matchingCar.vehicle);
|
this.updateVehicleInfo(matchingCar.vin, matchingCar.vehicle);
|
||||||
this.navigateForwardWithSingleCarMatch();
|
this.navigateForwardWithSingleCarMatch();
|
||||||
} else {
|
} else {
|
||||||
// if there are no matches or there are multiple matches, navigate to "address-vehicles" page
|
// if there are no matches or there are multiple matches, navigate to "address-vehicles" page
|
||||||
this.$router.navigateAfterSave(this.navigationScenarios.CONTINUING_WITH_MULTIPLE_VEHICLES, this.$route, {}, {}, carsFound);
|
this.$router.navigateAfterSave(this.navigationScenarios.CONTINUING_WITH_MULTIPLE_VEHICLES, this.$route, {}, {}, carsFound);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
validateZip(zip) {
|
validateZip(zip) {
|
||||||
return this.dispatchStoreAction(
|
return this.dispatchStoreAction(
|
||||||
storeActions.VALIDATE_ZIP,
|
storeActions.VALIDATE_ZIP,
|
||||||
{ zip });
|
{ zip });
|
||||||
},
|
},
|
||||||
lookupVin(lastName, streetAddress, zip, state) {
|
lookupVin(lastName, streetAddress, zip, state) {
|
||||||
return this.dispatchStoreAction(
|
return this.dispatchStoreAction(
|
||||||
storeActions.LOOKUP_VIN_BY_ADDRESS,
|
storeActions.LOOKUP_VIN_BY_ADDRESS,
|
||||||
{
|
{
|
||||||
licenseLastName: lastName,
|
licenseLastName: lastName,
|
||||||
licenseStreetAddress: streetAddress,
|
licenseStreetAddress: streetAddress,
|
||||||
licenseZip: zip,
|
licenseZip: zip,
|
||||||
licenseState: state
|
licenseState: state
|
||||||
}, false
|
}, false
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -342,7 +342,7 @@ export default {
|
||||||
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, vehicleInfo.imageUrl);
|
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, vehicleInfo.imageUrl);
|
||||||
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, vehicleInfo.imageVifNumber);
|
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, vehicleInfo.imageVifNumber);
|
||||||
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, vehicleInfo.imageColor);
|
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, vehicleInfo.imageColor);
|
||||||
},
|
},
|
||||||
updateCustomerInfo(serviceState) {
|
updateCustomerInfo(serviceState) {
|
||||||
store.commit(storeMutations.UPDATE_REGISTRATION_ADDRESS, this.customerQuestions.addressQuestions.streetAddress);
|
store.commit(storeMutations.UPDATE_REGISTRATION_ADDRESS, this.customerQuestions.addressQuestions.streetAddress);
|
||||||
store.commit(storeMutations.UPDATE_REGISTRATION_CITY, this.customerQuestions.addressQuestions.city);
|
store.commit(storeMutations.UPDATE_REGISTRATION_CITY, this.customerQuestions.addressQuestions.city);
|
||||||
|
|
@ -350,10 +350,10 @@ export default {
|
||||||
store.commit(storeMutations.UPDATE_REGISTRATION_ZIP_CODE, this.customerQuestions.addressQuestions.zipCode);
|
store.commit(storeMutations.UPDATE_REGISTRATION_ZIP_CODE, this.customerQuestions.addressQuestions.zipCode);
|
||||||
store.commit(storeMutations.UPDATE_REGISTRATION_FIRST_NAME, this.customerQuestions.firstName);
|
store.commit(storeMutations.UPDATE_REGISTRATION_FIRST_NAME, this.customerQuestions.firstName);
|
||||||
store.commit(storeMutations.UPDATE_REGISTRATION_LAST_NAME, this.customerQuestions.lastName);
|
store.commit(storeMutations.UPDATE_REGISTRATION_LAST_NAME, this.customerQuestions.lastName);
|
||||||
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP_CODE, this.serviceZipCode);
|
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP_CODE, this.serviceZipCode);
|
||||||
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_STATE, serviceState);
|
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_STATE, serviceState);
|
||||||
store.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, this.customerQuestions.emailAddress);
|
store.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, this.customerQuestions.emailAddress);
|
||||||
},
|
},
|
||||||
updateServiceLocationIfNecessary() {
|
updateServiceLocationIfNecessary() {
|
||||||
const serviceLocation = store.getters.order.serviceLocation;
|
const serviceLocation = store.getters.order.serviceLocation;
|
||||||
|
|
||||||
|
|
@ -373,7 +373,7 @@ export default {
|
||||||
},
|
},
|
||||||
AlertNonServiceableZipBody() {
|
AlertNonServiceableZipBody() {
|
||||||
return this.getCmsContent("AlertNonServiceableZipWidget", "BodyText");
|
return this.getCmsContent("AlertNonServiceableZipWidget", "BodyText");
|
||||||
},
|
},
|
||||||
AlertMatchedDifferentVehicleHeader() {
|
AlertMatchedDifferentVehicleHeader() {
|
||||||
const text = this.getCmsContent("AlertMatchedDifferentVehicleWidget", "HeadlineText").replaceAll("{custom:glassText}", getDamageString());
|
const text = this.getCmsContent("AlertMatchedDifferentVehicleWidget", "HeadlineText").replaceAll("{custom:glassText}", getDamageString());
|
||||||
return text;
|
return text;
|
||||||
|
|
@ -386,13 +386,13 @@ export default {
|
||||||
const content = this.getCmsContent("AlertMatchedDifferentVehicleWidget", "BodyText")
|
const content = this.getCmsContent("AlertMatchedDifferentVehicleWidget", "BodyText")
|
||||||
.replaceAll("{custom:glassText}", getDamageString())
|
.replaceAll("{custom:glassText}", getDamageString())
|
||||||
.replaceAll("{custom:vinYmmFound}", vinYmmFound)
|
.replaceAll("{custom:vinYmmFound}", vinYmmFound)
|
||||||
.replaceAll("{custom:vinYmmExpected}", vinYmmExpected);
|
.replaceAll("{custom:vinYmmExpected}", vinYmmExpected);
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
customerQuestions: {
|
customerQuestions: {
|
||||||
handler(newValue) {
|
handler(newValue) {
|
||||||
// if they modify one of the lookup fields (address, city, state, zipCode, or lastName), then modify the button text back to “Get my personalized quote”
|
// if they modify one of the lookup fields (address, city, state, zipCode, or lastName), then modify the button text back to “Get my personalized quote”
|
||||||
this.$refs.funnelFooter.updateButtonText(this.getCmsContent("FunnelFooterWidget", "ForwardButtonText"));
|
this.$refs.funnelFooter.updateButtonText(this.getCmsContent("FunnelFooterWidget", "ForwardButtonText"));
|
||||||
|
|
@ -412,8 +412,8 @@ export default {
|
||||||
// if the Service Zip Code field is ever hidden, clear out it's value
|
// if the Service Zip Code field is ever hidden, clear out it's value
|
||||||
if (!newValue) {
|
if (!newValue) {
|
||||||
this.serviceZipCode = null;
|
this.serviceZipCode = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
|
|
||||||
|
|
@ -223,7 +223,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>
|
||||||
|
|
@ -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: {
|
||||||
|
|
@ -170,14 +170,21 @@ describe("vehicle-parts.vue", () => {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
//Assert
|
//Assert
|
||||||
expect(wrapper.vm.glassParts).toEqual({ "Rear-Stationary": { "Rear": ['DB12209YPYNOEM'] } });
|
expect(wrapper.vm.glassParts).toEqual({ "Rear-Stationary": {
|
||||||
|
partNumber: "DB12209YPYNOEM",
|
||||||
|
description: "heated glass, solar, 1 hole",
|
||||||
|
color: "Gray Tint Privacy",
|
||||||
|
requiresRecalibration: false,
|
||||||
|
requiresCapabilityQuestions: false,
|
||||||
|
childParts: null
|
||||||
|
}});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("BackButtonAction triggers a router.navigate change", async () => {
|
test("BackButtonAction triggers a router.navigate change", async () => {
|
||||||
|
|
||||||
//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: {
|
||||||
|
|
@ -215,7 +222,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 }
|
||||||
|
|
||||||
|
|
||||||
store.commit = jest.fn();
|
store.commit = jest.fn();
|
||||||
|
|
@ -238,7 +245,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(
|
||||||
|
|
@ -285,13 +292,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 };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,13 @@
|
||||||
@submit="onSubmit"
|
@submit="onSubmit"
|
||||||
@invalid-submit="onInvalidSubmit"
|
@invalid-submit="onInvalidSubmit"
|
||||||
ref="theForm"
|
ref="theForm"
|
||||||
v-slot="{ meta }"
|
|
||||||
>
|
>
|
||||||
<div class="page-container-grouped-styles vehicle-parts">
|
<div class="page-container-grouped-styles vehicle-parts">
|
||||||
<funnelHeader ref="funnelHeader" cmsWidgetName="FunnelHeaderWidget" />
|
<funnelHeader ref="funnelHeader" cmsWidgetName="FunnelHeaderWidget" />
|
||||||
<vehicleBanner ref="vehicleBanner" cmsWidgetName="VehicleBannerWidget" :displayGenericVehicleImage="false" />
|
<vehicleBanner ref="vehicleBanner" cmsWidgetName="VehicleBannerWidget" :displayGenericVehicleImage="false" />
|
||||||
<funnelSubHeader ref="funnelSubHeader" cmsWidgetName="FunnelSubHeaderWidget" />
|
<funnelSubHeader ref="funnelSubHeader" cmsWidgetName="FunnelSubHeaderWidget" />
|
||||||
<div class="fade-on-route-transition sub-container make-tall">
|
<div class="fade-on-route-transition sub-container make-tall">
|
||||||
<div class="container-fluid prevent-squish my-5">
|
<div class="prevent-squish my-5">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<alert
|
<alert
|
||||||
|
|
@ -22,26 +21,22 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-for="(item, i) in PartsOrQuestions" :key="i">
|
||||||
<div v-for="(item, i) in PartsForQuestions" :key="i">
|
|
||||||
<!-- Render horizontal lines if there is multi-glass (aka if i > 0) -->
|
<!-- 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" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<glassPartQuestion
|
<glassPartQuestion
|
||||||
:ref="`${RefPrefix}-${item.glassLocation}-${item.glassName}`"
|
:ref="`${RefPrefix}-${item.glassLocation}-${item.glassName}`"
|
||||||
v-model="glassParts[item.glassLocation + '-' + item.glassName]"
|
v-model="glassParts[item.glassLocation + '-' + item.glassName]"
|
||||||
:glassLocation="item.glassLocation"
|
:glassLocation="item.glassLocation"
|
||||||
:glassName="item.glassName"
|
:glassName="item.glassName"
|
||||||
:colorAnswers="item.colorAnswers"
|
:colorAnswers="item.colorAnswers"
|
||||||
validationRules="replace-options-required"
|
:alreadyPopulatedPartsData="alreadyPopulatedPartsData"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<funnelFooter
|
<funnelFooter
|
||||||
cmsWidgetName="FunnelFooterWidget"
|
cmsWidgetName="FunnelFooterWidget"
|
||||||
ref="funnelFooter"
|
ref="funnelFooter"
|
||||||
:isForwardActionDisabled="!meta.valid"
|
:isForwardActionDisabled="isForwardActionDisabled"
|
||||||
@back-clicked="backButtonAction"
|
@back-clicked="backButtonAction"
|
||||||
@ForwardClicked="forwardButtonAction"
|
@ForwardClicked="forwardButtonAction"
|
||||||
/>
|
/>
|
||||||
|
|
@ -64,12 +59,7 @@ import { settleAllPromises } from "@/helpers/layout-helper";
|
||||||
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
|
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
|
||||||
import { storeMutations } from "@/constants/store-mutations";
|
import { storeMutations } from "@/constants/store-mutations";
|
||||||
import store from "@/store";
|
import store from "@/store";
|
||||||
import { Form, defineRule } from "vee-validate";
|
import { Form } from "vee-validate";
|
||||||
import { required } from "@/helpers/validation-rules";
|
|
||||||
import { errorMessages } from "@/constants/error-messages";
|
|
||||||
|
|
||||||
// DEFINE VALIDATION RULES
|
|
||||||
defineRule("replace-options-required", required(errorMessages.OPTION_REQUIRED));
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "vehicle-parts",
|
name: "vehicle-parts",
|
||||||
|
|
@ -88,12 +78,12 @@ 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,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
@ -101,6 +91,7 @@ data() {
|
||||||
return {
|
return {
|
||||||
glassParts: {},
|
glassParts: {},
|
||||||
alertWidgetData: Object,
|
alertWidgetData: Object,
|
||||||
|
alreadyPopulatedPartsData: {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
|
@ -113,7 +104,24 @@ components: {
|
||||||
alert,
|
alert,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
PartsForQuestions() {
|
isForwardActionDisabled() {
|
||||||
|
return Object.keys(this.matchedParts).length !== this.PartsFromApi.partsOrQuestions.length;
|
||||||
|
},
|
||||||
|
matchedParts() {
|
||||||
|
const matchedParts = [];
|
||||||
|
|
||||||
|
// Match them to the parts from the API.
|
||||||
|
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;
|
const partsData = this.PartsFromApi;
|
||||||
|
|
||||||
// Map API result data, to vehicle-parts data structure
|
// Map API result data, to vehicle-parts data structure
|
||||||
|
|
@ -165,40 +173,14 @@ methods: {
|
||||||
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
|
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
|
||||||
},
|
},
|
||||||
forwardButtonAction() {
|
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
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isMatched) {
|
|
||||||
matchedParts.push(currentPart);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no parts could be matched, throw an error.
|
// If no parts could be matched, throw an error.
|
||||||
if (matchedParts.length === 0) {
|
if (this.isForwardActionDisabled) {
|
||||||
|
this.$refs.funnelFooter.removeLoader();
|
||||||
throw new Error("Could not match any parts to the selected parts");
|
throw new Error("Could not match any parts to the selected parts");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save parts to the store.
|
// Save parts to the store.
|
||||||
store.commit(storeMutations.UPDATE_GLASS_PARTS, matchedParts);
|
store.commit(storeMutations.UPDATE_GLASS_PARTS, this.matchedParts);
|
||||||
|
|
||||||
// Navigate to the next page.
|
// Navigate to the next page.
|
||||||
this.$router.navigateAfterSave(
|
this.$router.navigateAfterSave(
|
||||||
|
|
@ -212,25 +194,14 @@ methods: {
|
||||||
},
|
},
|
||||||
|
|
||||||
LoadInitialPartsData() {
|
LoadInitialPartsData() {
|
||||||
const partsData = this.PartsFromApi;
|
this.alreadyPopulatedPartsData = this.$store.getters.lineItems.glassParts ?? [];
|
||||||
const alreadyPopulatedPartsData =
|
const savedPartNumbers = this.alreadyPopulatedPartsData.map(part => part.partNumber)
|
||||||
this.$store.getters.lineItems.glassParts === null
|
this.PartsFromApi.partsOrQuestions.forEach(glass => {
|
||||||
? {}
|
const savedPart = glass.parts.filter(part => savedPartNumbers.includes(part.partNumber))[0];
|
||||||
: this.$store.getters.lineItems.glassParts;
|
if (savedPart) {
|
||||||
|
this.glassParts[`${glass.glassLocation}-${glass.glassName}`.replace(" ", "-")] = savedPart;
|
||||||
partsData.partsOrQuestions.map((g) => {
|
}
|
||||||
// If the part is already populated, use the value from the store and populate the v-model.
|
});
|
||||||
Object.keys(alreadyPopulatedPartsData).forEach((key) => {
|
|
||||||
const partNumber = alreadyPopulatedPartsData[key].partNumber;
|
|
||||||
g.parts.forEach((p) => {
|
|
||||||
if (p.partNumber === partNumber) {
|
|
||||||
this.glassParts[g.glassLocation + "-" + g.glassName] = {
|
|
||||||
[g.glassLocation]: [partNumber],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -183,15 +183,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() {
|
||||||
|
|
@ -239,12 +234,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;
|
||||||
|
|
|
||||||
|
|
@ -223,18 +223,26 @@ export default {
|
||||||
span {
|
span {
|
||||||
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