Merge branch 'develop' into feature/CSR-108
This commit is contained in:
commit
6361f41672
23 changed files with 593 additions and 496 deletions
|
|
@ -10,6 +10,7 @@ describe("buttonQuestion.vue", () => {
|
|||
const wrapper = shallowMount(buttonQuestion, {
|
||||
propsData: {
|
||||
isOverflowScrollable: true,
|
||||
groupName: "group-name"
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -25,6 +26,7 @@ describe("buttonQuestion.vue", () => {
|
|||
const wrapper = shallowMount(buttonQuestion, {
|
||||
propsData: {
|
||||
buttonType: "listCard",
|
||||
groupName: "group-name"
|
||||
}
|
||||
});
|
||||
// Assert
|
||||
|
|
@ -39,6 +41,7 @@ describe("buttonQuestion.vue", () => {
|
|||
const wrapper = shallowMount(buttonQuestion, {
|
||||
propsData: {
|
||||
buttonType: "listButtonHorizontal",
|
||||
groupName: "group-name"
|
||||
}
|
||||
});
|
||||
// Assert
|
||||
|
|
@ -53,6 +56,7 @@ describe("buttonQuestion.vue", () => {
|
|||
const wrapper = shallowMount(buttonQuestion, {
|
||||
propsData: {
|
||||
buttonType: "radio",
|
||||
groupName: "group-name"
|
||||
}
|
||||
});
|
||||
// Assert
|
||||
|
|
@ -77,7 +81,8 @@ describe("buttonQuestion.vue", () => {
|
|||
// Act
|
||||
const localThis = {
|
||||
isWide: false,
|
||||
answers: ['a', 'b']
|
||||
answers: ['a', 'b'],
|
||||
groupName: "group-name"
|
||||
}
|
||||
|
||||
expect(buttonQuestion.computed.getColLength.call(localThis)).toBe("");
|
||||
|
|
@ -109,7 +114,7 @@ describe("buttonQuestion.vue", () => {
|
|||
describe("buttonQuestion.vue", () => {
|
||||
it("Should trigger event modelValue change to new value on when radio button selected", async () => {
|
||||
// Act
|
||||
const wrapper = shallowMount(buttonQuestion, setupMocks({}));
|
||||
const wrapper = shallowMount(buttonQuestion, setupMocks({propsData: {groupName: "group-name"}}));
|
||||
await wrapper.setProps({
|
||||
answers: ["2022", "2021", "2020"],
|
||||
isMultiSelect: false,
|
||||
|
|
@ -129,6 +134,7 @@ describe("buttonQuestion.vue", () => {
|
|||
propsData: {
|
||||
modelValue: ["2022", "2021", "2020"],
|
||||
isMultiSelect: true,
|
||||
groupName: "group-name"
|
||||
}
|
||||
}));
|
||||
const val = { checkValue: true, value: "2019", }
|
||||
|
|
@ -144,7 +150,8 @@ describe("buttonQuestion.vue", () => {
|
|||
const wrapper = shallowMount(buttonQuestion, setupMocks({
|
||||
propsData: {
|
||||
isMultiSelect: true,
|
||||
modelValue: ['a', 'b']
|
||||
modelValue: ['a', 'b'],
|
||||
groupName: "group-name"
|
||||
}
|
||||
}));
|
||||
const val = { checkValue: true, value: "2021", }
|
||||
|
|
@ -161,7 +168,8 @@ describe("buttonQuestion.vue", () => {
|
|||
const wrapper = shallowMount(buttonQuestion, setupMocks({
|
||||
propsData: {
|
||||
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 = {}) {
|
||||
const defaultMountOptions = { route: { query: { fmgPage: 'page-name' } } };
|
||||
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 -->
|
||||
<template>
|
||||
<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>
|
||||
</div>
|
||||
<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">
|
||||
<legend class="sr-only" :data-focus-target="groupName" :id="groupName" tabindex="-1">
|
||||
<fieldset class="w-100" :aria-required=isRequired :class="getFieldSetClasses" :role="isMultiSelect ? 'group' : 'radiogroup'" :aria-labelledby="formattedGroupName">
|
||||
<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.' }}
|
||||
</legend>
|
||||
<div :class="getComponentWrapperClasses">
|
||||
<div :class="getComponentLoopWrapperClasses">
|
||||
<div :class="getComponentWrapperClasses" v-for="answer in answers" :key="answer.Name ? answer.Name : answer">
|
||||
<component
|
||||
:is="buttonType"
|
||||
v-for="answer in answers"
|
||||
:key="answer.Name ? answer.Name : answer"
|
||||
@isCheckedChanged="handleCheckedChanged"
|
||||
:buttonID="answer.Name ? groupName + '-' + answer.Name : groupName + '-' + answer"
|
||||
:buttonID="answer.Name ? formattedGroupName + '-' + answer.Name : formattedGroupName + '-' + answer"
|
||||
:value="getValues(answer)"
|
||||
:buttonLabel="answer.Text ? answer.Text : answer"
|
||||
:buttonLabelSubCopy="answer.SubText"
|
||||
:textPosition="textPosition"
|
||||
:isMultiSelect="isMultiSelect"
|
||||
:groupName="groupName"
|
||||
:groupName="formattedGroupName"
|
||||
:selectingInitiatesLoad="selectingInitiatesLoad"
|
||||
:loaderColor="loaderColor"
|
||||
:loaderPosition="loaderPosition"
|
||||
|
|
@ -38,11 +37,18 @@
|
|||
:class="[suppressError ? 'alertError' : '']"
|
||||
: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>
|
||||
</fieldset>
|
||||
</div>
|
||||
<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>
|
||||
</template>
|
||||
|
|
@ -81,22 +87,31 @@ export default {
|
|||
isRequired: Boolean,
|
||||
isOverflowScrollable: Boolean,
|
||||
isWide: Boolean,
|
||||
modelValue: Array,
|
||||
modelValue: [Array, String],
|
||||
validationRules: String,
|
||||
suppressError: Boolean,
|
||||
useTextForValue: Boolean,
|
||||
clearOnUnmount: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
getFieldSetClasses() {
|
||||
return this.isOverflowScrollable
|
||||
? "container-fluid overflow-scroll position-absolute px-5 pt-1 py-0"
|
||||
: "";
|
||||
formattedGroupName() {
|
||||
return this.groupName.replace(" ", "-");
|
||||
},
|
||||
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;
|
||||
switch (this.buttonType) {
|
||||
case "listButton":
|
||||
|
|
@ -106,7 +121,7 @@ export default {
|
|||
classes = "d-flex flex-row p-0";
|
||||
break;
|
||||
case 'listCard':
|
||||
classes = 'row justify-content-center g-2'
|
||||
classes = "row g-2 justify-content-center";
|
||||
break;
|
||||
case 'radio':
|
||||
classes = 'ui-radio d-flex'
|
||||
|
|
@ -114,11 +129,22 @@ export default {
|
|||
}
|
||||
return classes;
|
||||
},
|
||||
getComponentWrapperClasses() {
|
||||
let classes = "";
|
||||
|
||||
classes += this.isWide ? "col-12" : "col";
|
||||
|
||||
if (this.buttonType == "radio") {
|
||||
classes += " radio-button-container";
|
||||
}
|
||||
|
||||
return classes;
|
||||
},
|
||||
getColLength(){
|
||||
if(this.isWide) {
|
||||
return "12"
|
||||
} else {
|
||||
return this.answers.length < 3 ? '' : '-4';
|
||||
return "";
|
||||
}
|
||||
},
|
||||
selectedValues: {
|
||||
|
|
@ -135,6 +161,7 @@ export default {
|
|||
if (this.useTextForValue){
|
||||
return answer.Text
|
||||
}
|
||||
|
||||
return answer.Name ? answer.Name : answer;
|
||||
},
|
||||
handleCheckedChanged(val) {
|
||||
|
|
@ -146,7 +173,12 @@ export default {
|
|||
val.checkValue ? newSelectedValues.push(val.value) : newSelectedValues.splice(newSelectedValues.indexOf(val.value), 1);
|
||||
this.selectedValues = newSelectedValues;
|
||||
}
|
||||
else {
|
||||
this.selectedValues = val.value;
|
||||
}
|
||||
}
|
||||
|
||||
this.$emit("isCheckedChanged", val);
|
||||
},
|
||||
},
|
||||
components: {
|
||||
|
|
@ -172,6 +204,12 @@ export default {
|
|||
}
|
||||
.button-question {
|
||||
color: $black;
|
||||
|
||||
.radio-button-container {
|
||||
&:not(:last-child) {
|
||||
padding-bottom: map-get($spacers, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
.question-text {
|
||||
margin-top: 1.5rem;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const maska = jest.fn();
|
|||
describe("textboxQuestion.vue", () => {
|
||||
|
||||
it("Should render a text input", async () => {
|
||||
// Arrange
|
||||
// Arrange
|
||||
const wrapper = shallowMount(textboxQuestion, {
|
||||
global: {
|
||||
directives: {
|
||||
|
|
@ -53,35 +53,7 @@ describe("textboxQuestion.vue", () => {
|
|||
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 () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(textboxQuestion, {
|
||||
|
|
@ -103,7 +75,7 @@ describe("textboxQuestion.vue", () => {
|
|||
expect(input.attributes().id).toEqual("input ID");
|
||||
|
||||
});
|
||||
|
||||
|
||||
it("Should render the 'questionText' data value as the aria-label attribute.", async () => {
|
||||
|
||||
// Arrange
|
||||
|
|
@ -193,6 +165,6 @@ describe("textboxQuestion.vue", () => {
|
|||
// Assert
|
||||
expect(wrapper.vm.handleChange).toHaveBeenCalled;
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<template>
|
||||
<div class="textbox-question" :class="(errors && errors.length) || hasError ? 'has-error' : ''">
|
||||
<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
|
||||
v-model.trim="value"
|
||||
v-maska="mask"
|
||||
|
|
@ -14,7 +13,6 @@
|
|||
:aria-disabled="isDisabled"
|
||||
:disabled="isDisabled"
|
||||
:aria-required="isRequired"
|
||||
autocomplete="do-not-autofill"
|
||||
:class="[hasIcon ? 'has-icon' : '', iconRight ? 'icon-right' : '']"
|
||||
:validationRules="validationRules"
|
||||
@change="handleChange"
|
||||
|
|
@ -46,7 +44,6 @@ export default {
|
|||
inputId: String,
|
||||
isDisabled: Boolean,
|
||||
isRequired: Boolean,
|
||||
disableAutoFill: Boolean,
|
||||
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
|
||||
hasError: Boolean,
|
||||
|
|
@ -132,7 +129,7 @@ export default {
|
|||
const result = await validate(newValue, this.validationRules); // do a test validation check, without triggering full validation
|
||||
if (result.valid) {
|
||||
this.handleChange(newValue); // trigger full validation on this field only
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<loadingModal ref="loadingModal"/>
|
||||
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
|
||||
<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">
|
||||
<customerQuestions ref="customerQuestions" v-model="customerQuestions" />
|
||||
<alert ref="alertVinNotFound" v-if="displayVinNotFoundAlert"
|
||||
|
|
@ -24,14 +24,14 @@
|
|||
:manualCopy="AlertMatchedDifferentVehicleBody"
|
||||
alertClass="alert-warning"
|
||||
v-bind:isDismissible="false"
|
||||
/>
|
||||
/>
|
||||
<alert ref="alertNonServiceableZip" v-if="displayNonServiceableZipAlert"
|
||||
class="mb-4"
|
||||
alertClass="alert-danger"
|
||||
:manualHeadline="AlertNonServiceableZipHeader"
|
||||
:manualCopy="AlertNonServiceableZipBody"
|
||||
v-bind:isDismissible="false"
|
||||
/>
|
||||
/>
|
||||
<alert ref="alertVinLookupsByHomeAddressNotAllowed" v-if="displayVinLookupByHomeAddressNotAllowedAlert"
|
||||
class="mb-4"
|
||||
cmsWidgetName="AlertVinLookupsByHomeAddressNotAllowedWidget"
|
||||
|
|
@ -42,11 +42,11 @@
|
|||
<div class="service-zip-field" v-if="showServiceZipField" aria-live="polite">
|
||||
<div class="row mb-4">
|
||||
<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>
|
||||
</transition>
|
||||
</transition>
|
||||
<funnel-footer
|
||||
cmsWidgetName="FunnelFooterWidget"
|
||||
ref="funnelFooter"
|
||||
|
|
@ -55,7 +55,7 @@
|
|||
@back-clicked="backButtonAction"
|
||||
:isForwardActionDisabled="!meta.valid"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</template>
|
||||
|
|
@ -122,14 +122,14 @@ export default {
|
|||
firstName: this.getRegistrationFirstNameFromStore(),
|
||||
lastName: this.getRegistrationLastNameFromStore(),
|
||||
emailAddress: this.getEmailFromStore(),
|
||||
},
|
||||
},
|
||||
serviceZipCode: this.getServiceZipFromStore(),
|
||||
displayNonServiceableZipAlert: false,
|
||||
displayVinNotFoundAlert: false,
|
||||
displayMatchedDifferentVehicleAlert: false,
|
||||
displayVinLookupByHomeAddressNotAllowedAlert: false,
|
||||
previouslyEnteredCarId: "",
|
||||
isSelectedGlassAvailableForVehicle: false,
|
||||
isSelectedGlassAvailableForVehicle: false,
|
||||
customAlertData: {},
|
||||
showServiceZipField: this.getServiceZipFromStore(),
|
||||
isZipServicable: false,
|
||||
|
|
@ -150,7 +150,7 @@ export default {
|
|||
this.$route
|
||||
);
|
||||
},
|
||||
attachCustomEvents() {
|
||||
attachCustomEvents() {
|
||||
this.prependActionToMethod(this, this.forwardButtonAction, () => {
|
||||
this.pushEventToGA(
|
||||
this.$route.query[this.queryStrings.FMG_PAGE],
|
||||
|
|
@ -177,7 +177,7 @@ export default {
|
|||
},
|
||||
getRegistrationLastNameFromStore() {
|
||||
return store.getters.vehicle.registration.lastName;
|
||||
},
|
||||
},
|
||||
getEmailFromStore() {
|
||||
return store.getters.order.customer.emailAddress;
|
||||
},
|
||||
|
|
@ -189,9 +189,9 @@ export default {
|
|||
|
||||
// Lookup VIN(s) with the provided address
|
||||
const vinLookupPromise = this.lookupVin(
|
||||
this.customerQuestions.lastName,
|
||||
this.customerQuestions.addressQuestions.streetAddress,
|
||||
this.customerQuestions.addressQuestions.zipCode,
|
||||
this.customerQuestions.lastName,
|
||||
this.customerQuestions.addressQuestions.streetAddress,
|
||||
this.customerQuestions.addressQuestions.zipCode,
|
||||
this.customerQuestions.addressQuestions.state
|
||||
);
|
||||
|
||||
|
|
@ -200,8 +200,8 @@ export default {
|
|||
|
||||
const vinLookupResponse = await vinLookupPromise;
|
||||
const serviceZipValidationResponse = await serviceZipValidationPromise;
|
||||
|
||||
if (!vinLookupResponse.data.isStatePermissible) {
|
||||
|
||||
if (!vinLookupResponse.data.isStatePermissible) {
|
||||
// State Restrictions forbid lookup by address
|
||||
this.displayVinLookupByHomeAddressNotAllowedAlert = true;
|
||||
this.$refs.funnelFooter.removeLoader();
|
||||
|
|
@ -210,29 +210,29 @@ export default {
|
|||
|
||||
// if the neither the registration zip code or service zip code are not serviceable
|
||||
this.isZipServicable = serviceZipValidationResponse.data.isServiceable;
|
||||
if (!this.isZipServicable) {
|
||||
if (!this.isZipServicable) {
|
||||
this.displayNonServiceableZipAlert = true;
|
||||
this.showServiceZipField = true;
|
||||
this.$refs.funnelFooter.removeLoader();
|
||||
this.$refs.funnelFooter.removeLoader();
|
||||
} else if (!this.serviceZipCode) {
|
||||
// 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
|
||||
this.serviceZipCode = this.customerQuestions.addressQuestions.zipCode;
|
||||
}
|
||||
}
|
||||
|
||||
const carEntered = store.getters.vehicle;
|
||||
const carsFound = vinLookupResponse.data.vinVehicles;
|
||||
|
||||
if (carsFound.length == 0) {
|
||||
// No VINs found
|
||||
// No VINs found
|
||||
this.displayVinNotFoundAlert = true;
|
||||
this.$refs.funnelFooter.removeLoader();
|
||||
return;
|
||||
} else if (carsFound.length == 1) {
|
||||
return;
|
||||
} else if (carsFound.length == 1) {
|
||||
const carFound = carsFound[0].vehicle;
|
||||
this.isCarIdDifferent = carFound.carId !== carEntered.carId;
|
||||
|
||||
if (this.isCarIdDifferent && carFound.carId !== this.previouslyEnteredCarId) {
|
||||
if (this.isCarIdDifferent && carFound.carId !== this.previouslyEnteredCarId) {
|
||||
// Display Alert
|
||||
this.previouslyEnteredCarId = carFound.carId;
|
||||
this.customAlertData.vehicleInfo = carFound;
|
||||
|
|
@ -241,12 +241,12 @@ export default {
|
|||
this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId(carFound.carId);
|
||||
|
||||
// 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();
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!this.isZipServicable) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -254,21 +254,21 @@ export default {
|
|||
// update data if the zip or service zip is servicable
|
||||
this.updateVehicleInfo(carsFound[0].vin, carFound);
|
||||
this.updateCustomerInfo(serviceZipValidationResponse.data.state);
|
||||
|
||||
|
||||
} else if (carsFound.length > 1) {
|
||||
if (!this.isZipServicable) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// and one and only one of them matches the carId entered, save the vehicle info
|
||||
// so we can go to the Heritage Funnel directly
|
||||
const matchingCar = matchingCars[0];
|
||||
this.updateVehicleInfo(matchingCar.vin, matchingCar.vehicle);
|
||||
}
|
||||
|
||||
|
||||
// update data if the zip or service zip is servicable
|
||||
this.updateCustomerInfo(serviceZipValidationResponse.data.state);
|
||||
}
|
||||
|
|
@ -277,13 +277,13 @@ export default {
|
|||
},
|
||||
resetWarningsAndErrors() {
|
||||
this.displayVinNotFoundAlert = false;
|
||||
this.displayNonServiceableZipAlert = false;
|
||||
this.displayNonServiceableZipAlert = false;
|
||||
this.displayMatchedDifferentVehicleAlert = false;
|
||||
this.displayVinLookupByHomeAddressNotAllowedAlert = false;
|
||||
},
|
||||
navigateForward(carEntered, carsFound) {
|
||||
this.updateServiceLocationIfNecessary();
|
||||
|
||||
|
||||
if (carsFound.length == 1) {
|
||||
// if a different vehicle is found than the one entered and the selected glass
|
||||
// is not available for that vehicle
|
||||
|
|
@ -297,37 +297,37 @@ export default {
|
|||
}, {}
|
||||
);
|
||||
} else {
|
||||
// otherwise
|
||||
// otherwise
|
||||
this.navigateForwardWithSingleCarMatch();
|
||||
}
|
||||
}
|
||||
} else if (carsFound.length > 1) {
|
||||
// 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) {
|
||||
// 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.navigateForwardWithSingleCarMatch();
|
||||
} else {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
validateZip(zip) {
|
||||
return this.dispatchStoreAction(
|
||||
storeActions.VALIDATE_ZIP,
|
||||
storeActions.VALIDATE_ZIP,
|
||||
{ zip });
|
||||
},
|
||||
lookupVin(lastName, streetAddress, zip, state) {
|
||||
return this.dispatchStoreAction(
|
||||
storeActions.LOOKUP_VIN_BY_ADDRESS,
|
||||
{
|
||||
licenseLastName: lastName,
|
||||
licenseStreetAddress: streetAddress,
|
||||
licenseZip: zip,
|
||||
licenseState: state
|
||||
{
|
||||
licenseLastName: lastName,
|
||||
licenseStreetAddress: streetAddress,
|
||||
licenseZip: zip,
|
||||
licenseState: state
|
||||
}, false
|
||||
);
|
||||
},
|
||||
|
|
@ -342,7 +342,7 @@ export default {
|
|||
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_COLOR, vehicleInfo.imageColor);
|
||||
},
|
||||
},
|
||||
updateCustomerInfo(serviceState) {
|
||||
store.commit(storeMutations.UPDATE_REGISTRATION_ADDRESS, this.customerQuestions.addressQuestions.streetAddress);
|
||||
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_FIRST_NAME, this.customerQuestions.firstName);
|
||||
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_STATE, serviceState);
|
||||
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP_CODE, this.serviceZipCode);
|
||||
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_STATE, serviceState);
|
||||
store.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, this.customerQuestions.emailAddress);
|
||||
},
|
||||
},
|
||||
updateServiceLocationIfNecessary() {
|
||||
const serviceLocation = store.getters.order.serviceLocation;
|
||||
|
||||
|
|
@ -373,7 +373,7 @@ export default {
|
|||
},
|
||||
AlertNonServiceableZipBody() {
|
||||
return this.getCmsContent("AlertNonServiceableZipWidget", "BodyText");
|
||||
},
|
||||
},
|
||||
AlertMatchedDifferentVehicleHeader() {
|
||||
const text = this.getCmsContent("AlertMatchedDifferentVehicleWidget", "HeadlineText").replaceAll("{custom:glassText}", getDamageString());
|
||||
return text;
|
||||
|
|
@ -386,13 +386,13 @@ export default {
|
|||
const content = this.getCmsContent("AlertMatchedDifferentVehicleWidget", "BodyText")
|
||||
.replaceAll("{custom:glassText}", getDamageString())
|
||||
.replaceAll("{custom:vinYmmFound}", vinYmmFound)
|
||||
.replaceAll("{custom:vinYmmExpected}", vinYmmExpected);
|
||||
.replaceAll("{custom:vinYmmExpected}", vinYmmExpected);
|
||||
|
||||
return content;
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
customerQuestions: {
|
||||
customerQuestions: {
|
||||
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”
|
||||
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 (!newValue) {
|
||||
this.serviceZipCode = null;
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -223,7 +223,11 @@ export default {
|
|||
handler() {
|
||||
// does this vehicle match the previously selected 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
|
||||
},
|
||||
|
|
|
|||
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"
|
||||
groupName="ChooseVehicleMake"
|
||||
textPosition="text-start"
|
||||
v-model="selectedValueAsArray"
|
||||
v-model="selectedValue"
|
||||
isRequired=true
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -34,14 +34,13 @@ export default {
|
|||
questionText(){
|
||||
return this.getCmsContent(this.cmsWidgetName, 'QuestionText');
|
||||
},
|
||||
selectedValueAsArray: {
|
||||
selectedValue: {
|
||||
get: function() {
|
||||
const modelValueAsArray = this.modelValue ? [this.modelValue] : [];
|
||||
return modelValueAsArray;
|
||||
},
|
||||
set: function(newValue) {
|
||||
const newValueAsScalar = newValue && newValue.length > 0 ? newValue[0] : null;
|
||||
this.$emit("update:modelValue", newValueAsScalar);
|
||||
this.$emit("update:modelValue", newValue);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
:answers="models"
|
||||
groupName="ChooseVehicleModel"
|
||||
textPosition="text-start"
|
||||
v-model="selectedValueAsArray"
|
||||
v-model="selectedValue"
|
||||
isRequired=true
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -34,14 +34,13 @@ export default {
|
|||
questionText(){
|
||||
return this.getCmsContent(this.cmsWidgetName, 'QuestionText');
|
||||
},
|
||||
selectedValueAsArray: {
|
||||
selectedValue: {
|
||||
get: function() {
|
||||
const modelValueAsArray = this.modelValue ? [this.modelValue] : [];
|
||||
return modelValueAsArray;
|
||||
},
|
||||
set: function(newValue) {
|
||||
const newValueAsScalar = newValue && newValue.length > 0 ? newValue[0] : null;
|
||||
this.$emit("update:modelValue", newValueAsScalar);
|
||||
this.$emit("update:modelValue", newValue);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -69,24 +69,25 @@ describe("glass-part-question.vue", () => {
|
|||
});
|
||||
|
||||
test("Should emit updateModelValue, and have correct attributes", async () => {
|
||||
|
||||
//Arrange
|
||||
|
||||
const { wrapper } = setupMocks(featureListData);
|
||||
store.getters.pageData.mockReset();
|
||||
store.getters.pageData.mockReturnValueOnce({ partsOrQuestions: [{glassName: "Stationary", glassLocation: "Rear", parts: [{ partNumber: "DB12209GTYN", color: "Green Tint"}]}] });
|
||||
|
||||
//Act
|
||||
await wrapper.vm.$nextTick();
|
||||
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
|
||||
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([{ selectedTint: 'Green Tint' }]);
|
||||
expect(listCard.attributes("buttonid")).toBe("Rear-Stationary-Green Tint");
|
||||
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([{ partNumber: "DB12209GTYN", color: "Green Tint"}]);
|
||||
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 () => {
|
||||
|
|
@ -96,21 +97,101 @@ describe("glass-part-question.vue", () => {
|
|||
|
||||
//Act
|
||||
await wrapper.vm.$nextTick();
|
||||
wrapper.setData({ selectedTint: { "Rear-Stationary": 'Green Tint' } });
|
||||
|
||||
expect(wrapper.vm.selectedTint).toStrictEqual({ "Rear-Stationary": 'Green Tint' });
|
||||
await wrapper.setData({ selectedTint: 'Green Tint', selectedPartNumber: "test" });
|
||||
|
||||
expect(wrapper.vm.selectedTint).toEqual('Green Tint');
|
||||
expect(wrapper.vm.selectedPartNumber).toEqual('test');
|
||||
|
||||
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
|
||||
store.getters.pageData.mockReturnValueOnce({ partsOrQuestions: {}});
|
||||
store.getters.pageData.mockReturnValueOnce({ partsOrQuestions: [{glassName: "Stationary", glassLocation: "Rear", parts: []}] });
|
||||
store.getters.lineItems = { glassParts: {} }
|
||||
|
||||
const mountOptions = getMountOptions({
|
||||
|
|
|
|||
|
|
@ -1,245 +1,230 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<p class="mb-0 color-question-text">{{ colorQuestionText }}</p>
|
||||
<p class="mb-0 color-question-text">{{ colorQuestionText }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="container nested-radio"
|
||||
v-for="(value, name, index) in featureListData"
|
||||
:key="index"
|
||||
>
|
||||
<div class="row my-2">
|
||||
<div class="col">
|
||||
<listCard
|
||||
v-model="selectedTint[name]"
|
||||
:value="`${glassLocation}-${glassName}-${name}`"
|
||||
:isRadio="true"
|
||||
:isWide="true"
|
||||
:buttonImage="
|
||||
require(`@/assets/img/tints/${getTintSourceImage(
|
||||
glassLocation,
|
||||
name
|
||||
)}`)
|
||||
"
|
||||
:buttonLabel="name"
|
||||
altText=""
|
||||
isRequired
|
||||
:buttonID="`${glassLocation}-${glassName}-${name}`"
|
||||
:groupName="`${glassLocation}-${glassName}`"
|
||||
@isCheckedChanged="ResetTintAndPartSelections()"
|
||||
:validationRules="validationRules"
|
||||
/>
|
||||
<div class="row form-test-error mt-1">
|
||||
<error-message :name="`${glassLocation}-${glassName}`" v-if="!suppressError"></error-message>
|
||||
<div class="nested-radio">
|
||||
<div class="row my-2">
|
||||
<div class="col">
|
||||
<buttonQuestion
|
||||
v-model="selectedTint"
|
||||
:answers="tintSelectionOptions"
|
||||
buttonType="listCard"
|
||||
:isWide="true"
|
||||
altText=""
|
||||
isRequired
|
||||
:groupName="`${glassLocation}-${glassName}`"
|
||||
@isCheckedChanged="ResetTintAndPartSelections"
|
||||
:validationRules="tintValidationRules"
|
||||
>
|
||||
<div class="row my-2" aria-live="polite">
|
||||
<div class="col">
|
||||
<buttonQuestion
|
||||
v-model="selectedPartNumber"
|
||||
buttonType="radio"
|
||||
class="radioQuestion"
|
||||
:questionText="glassFeatureQuestion"
|
||||
:answers="featureListData[selectedTint]"
|
||||
textPosition="text-start"
|
||||
:loaderEnabled="false"
|
||||
isRequired
|
||||
:groupName="`${glassLocation}-${glassName}-${selectedTint}`"
|
||||
:validationRules="partValidationRules"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</buttonQuestion>
|
||||
</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>
|
||||
|
||||
<script>
|
||||
// Components
|
||||
import listCard from "@/ux-components/list-card/list-card";
|
||||
import buttonQuestion from "@/common-components/button-question/button-question";
|
||||
|
||||
// Supporting files
|
||||
import { getTintImage } from "@/constants/tint-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 {
|
||||
name: "glass-part-question",
|
||||
inheritAttrs: false,
|
||||
data() {
|
||||
return {
|
||||
glassColorQuestion: "",
|
||||
glassFeatureQuestion: "",
|
||||
selectedTint: {}, // v-model for the list card selections
|
||||
};
|
||||
},
|
||||
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}`
|
||||
);
|
||||
name: "glass-part-question",
|
||||
inheritAttrs: false,
|
||||
data() {
|
||||
return {
|
||||
glassColorQuestion: "",
|
||||
glassFeatureQuestion: "",
|
||||
selectedTint: "",
|
||||
};
|
||||
},
|
||||
|
||||
selectedPart: {
|
||||
get: function () {
|
||||
return this.modelValue;
|
||||
},
|
||||
set: function (newValue) {
|
||||
this.$emit("update:modelValue", newValue);
|
||||
},
|
||||
props: {
|
||||
glassName: String,
|
||||
glassLocation: String,
|
||||
colorAnswers: Array,
|
||||
modelValue: Object,
|
||||
alreadyPopulatedPartsData: Array
|
||||
},
|
||||
|
||||
// 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;
|
||||
mounted() {
|
||||
this.LoadPreselectedValues();
|
||||
},
|
||||
|
||||
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;
|
||||
components: {
|
||||
buttonQuestion,
|
||||
},
|
||||
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.
|
||||
// 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.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}`,
|
||||
};
|
||||
}
|
||||
tintSelectionOptions() {
|
||||
let tintOptions = [];
|
||||
Object.keys(this.featureListData).forEach((tintOption) => {
|
||||
tintOptions.push({
|
||||
Name: tintOption,
|
||||
Text: tintOption,
|
||||
AnswerImageUrl: require(`@/assets/img/tints/${this.getTintSourceImage(
|
||||
this.glassLocation,
|
||||
tintOption
|
||||
)}`),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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>
|
||||
|
||||
<style lang="scss">
|
||||
.nested-radio {
|
||||
.ui-radio {
|
||||
flex-direction: column;
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
.ui-radio {
|
||||
flex-direction: column;
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
p {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.color-question-text {
|
||||
color: $black;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $black;
|
||||
font-weight: $font-weight-bold;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ describe("vehicle-parts.vue", () => {
|
|||
test("Set cms content called on load", async (done) => {
|
||||
//Arrange
|
||||
store.getters.pageData.mockReturnValue(basePartResponse);
|
||||
store.getters.lineItems = { glassParts: {} }
|
||||
store.getters.lineItems = { glassParts: null }
|
||||
|
||||
const { wrapper, apiPromise } = setupMocks(
|
||||
{
|
||||
|
|
@ -104,7 +104,7 @@ describe("vehicle-parts.vue", () => {
|
|||
|
||||
//Arrange
|
||||
store.getters.pageData.mockReturnValue(basePartResponse);
|
||||
store.getters.lineItems = { glassParts: {} }
|
||||
store.getters.lineItems = { glassParts: null }
|
||||
|
||||
const { wrapper } = setupMocks({
|
||||
mountOptionsMockData: {
|
||||
|
|
@ -141,7 +141,7 @@ describe("vehicle-parts.vue", () => {
|
|||
|
||||
//Arrange
|
||||
store.getters.pageData.mockReturnValueOnce(basePartResponse);
|
||||
store.getters.lineItems = { glassParts: { 0: { partNumber: 'DB12209YPYNOEM'} } }
|
||||
store.getters.lineItems = { glassParts: [{ partNumber: 'DB12209YPYNOEM'} ] }
|
||||
|
||||
const { wrapper } = setupMocks({
|
||||
mountOptionsMockData: {
|
||||
|
|
@ -170,14 +170,21 @@ describe("vehicle-parts.vue", () => {
|
|||
await nextTick();
|
||||
|
||||
//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 () => {
|
||||
|
||||
//Arrange
|
||||
store.getters.pageData.mockReturnValueOnce(basePartResponse);
|
||||
store.getters.lineItems = { glassParts: {} }
|
||||
store.getters.lineItems = { glassParts: null }
|
||||
|
||||
const { wrapper } = setupMocks({
|
||||
mountOptionsMockData: {
|
||||
|
|
@ -215,7 +222,7 @@ describe("vehicle-parts.vue", () => {
|
|||
|
||||
//Arrange
|
||||
store.getters.pageData.mockReturnValueOnce(basePartResponse);
|
||||
store.getters.lineItems = { glassParts: {} }
|
||||
store.getters.lineItems = { glassParts: null }
|
||||
|
||||
|
||||
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
|
||||
vehicleParts.beforeRouteEnter.call(
|
||||
|
|
@ -285,13 +292,14 @@ function setupMocks({ pageHeaderWidgetHeaderText = {}, mountOptionsMockData = {}
|
|||
settleAllPromises.mockImplementation(() => apiPromise);
|
||||
fetchCmsContentForPage.mockImplementation(() => Promise.resolve());
|
||||
|
||||
const mountOptions = getMountOptions(mountOptionsMockData,);
|
||||
const mountOptions = getMountOptions(mountOptionsMockData);
|
||||
const wrapper = shallowMount(vehicleParts, mountOptions);
|
||||
|
||||
const partQuestionRearWrapper = wrapper.findComponent({ name: "glassPartQuestion", });
|
||||
partQuestionRearWrapper.vm.initializeComponent = glassPartQuestion.methods.initializeComponent;
|
||||
|
||||
wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent;
|
||||
wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn();
|
||||
|
||||
return { wrapper, apiPromise };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,14 +3,13 @@
|
|||
@submit="onSubmit"
|
||||
@invalid-submit="onInvalidSubmit"
|
||||
ref="theForm"
|
||||
v-slot="{ meta }"
|
||||
>
|
||||
<div class="page-container-grouped-styles vehicle-parts">
|
||||
<funnelHeader ref="funnelHeader" cmsWidgetName="FunnelHeaderWidget" />
|
||||
<vehicleBanner ref="vehicleBanner" cmsWidgetName="VehicleBannerWidget" :displayGenericVehicleImage="false" />
|
||||
<funnelSubHeader ref="funnelSubHeader" cmsWidgetName="FunnelSubHeaderWidget" />
|
||||
<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="col">
|
||||
<alert
|
||||
|
|
@ -22,26 +21,22 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-for="(item, i) in PartsForQuestions" :key="i">
|
||||
<div v-for="(item, i) in PartsOrQuestions" :key="i">
|
||||
<!-- Render horizontal lines if there is multi-glass (aka if i > 0) -->
|
||||
<div class="container-fluid">
|
||||
<hr v-if="i > 0" />
|
||||
</div>
|
||||
|
||||
<hr v-if="i > 0" />
|
||||
<glassPartQuestion
|
||||
:ref="`${RefPrefix}-${item.glassLocation}-${item.glassName}`"
|
||||
v-model="glassParts[item.glassLocation + '-' + item.glassName]"
|
||||
:glassLocation="item.glassLocation"
|
||||
:glassName="item.glassName"
|
||||
:colorAnswers="item.colorAnswers"
|
||||
validationRules="replace-options-required"
|
||||
/>
|
||||
:ref="`${RefPrefix}-${item.glassLocation}-${item.glassName}`"
|
||||
v-model="glassParts[item.glassLocation + '-' + item.glassName]"
|
||||
:glassLocation="item.glassLocation"
|
||||
:glassName="item.glassName"
|
||||
:colorAnswers="item.colorAnswers"
|
||||
:alreadyPopulatedPartsData="alreadyPopulatedPartsData"
|
||||
/>
|
||||
</div>
|
||||
<funnelFooter
|
||||
cmsWidgetName="FunnelFooterWidget"
|
||||
ref="funnelFooter"
|
||||
:isForwardActionDisabled="!meta.valid"
|
||||
:isForwardActionDisabled="isForwardActionDisabled"
|
||||
@back-clicked="backButtonAction"
|
||||
@ForwardClicked="forwardButtonAction"
|
||||
/>
|
||||
|
|
@ -64,12 +59,7 @@ import { settleAllPromises } from "@/helpers/layout-helper";
|
|||
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
|
||||
import { storeMutations } from "@/constants/store-mutations";
|
||||
import store from "@/store";
|
||||
import { Form, defineRule } 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));
|
||||
import { Form } from "vee-validate";
|
||||
|
||||
export default {
|
||||
name: "vehicle-parts",
|
||||
|
|
@ -88,12 +78,12 @@ export default {
|
|||
|
||||
// Glass Part Question dynamic component
|
||||
Object.keys(vm.$refs)
|
||||
.filter((r) => r.includes(vm.RefPrefix) && vm.$refs[r][0] !== undefined)
|
||||
.forEach((c) =>
|
||||
vm.$refs[c][0].initializeComponent({
|
||||
ColorQuestionWidget: resultMap.cmsContent.ColorQuestionWidget,
|
||||
FeatureQuestionWidget: resultMap.cmsContent.FeatureQuestionWidget,
|
||||
})
|
||||
.filter((r) => r.includes(vm.RefPrefix) && vm.$refs[r][0] !== undefined)
|
||||
.forEach((c) =>
|
||||
vm.$refs[c][0].initializeComponent({
|
||||
ColorQuestionWidget: resultMap.cmsContent.ColorQuestionWidget,
|
||||
FeatureQuestionWidget: resultMap.cmsContent.FeatureQuestionWidget,
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
|
|
@ -101,6 +91,7 @@ data() {
|
|||
return {
|
||||
glassParts: {},
|
||||
alertWidgetData: Object,
|
||||
alreadyPopulatedPartsData: {}
|
||||
};
|
||||
},
|
||||
components: {
|
||||
|
|
@ -113,7 +104,24 @@ components: {
|
|||
alert,
|
||||
},
|
||||
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;
|
||||
|
||||
// Map API result data, to vehicle-parts data structure
|
||||
|
|
@ -165,40 +173,14 @@ methods: {
|
|||
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
|
||||
},
|
||||
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 (matchedParts.length === 0) {
|
||||
if (this.isForwardActionDisabled) {
|
||||
this.$refs.funnelFooter.removeLoader();
|
||||
throw new Error("Could not match any parts to the selected parts");
|
||||
}
|
||||
|
||||
// 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.
|
||||
this.$router.navigateAfterSave(
|
||||
|
|
@ -212,25 +194,14 @@ methods: {
|
|||
},
|
||||
|
||||
LoadInitialPartsData() {
|
||||
const partsData = this.PartsFromApi;
|
||||
const alreadyPopulatedPartsData =
|
||||
this.$store.getters.lineItems.glassParts === null
|
||||
? {}
|
||||
: this.$store.getters.lineItems.glassParts;
|
||||
|
||||
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],
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
this.alreadyPopulatedPartsData = this.$store.getters.lineItems.glassParts ?? [];
|
||||
const savedPartNumbers = this.alreadyPopulatedPartsData.map(part => part.partNumber)
|
||||
this.PartsFromApi.partsOrQuestions.forEach(glass => {
|
||||
const savedPart = glass.parts.filter(part => savedPartNumbers.includes(part.partNumber))[0];
|
||||
if (savedPart) {
|
||||
this.glassParts[`${glass.glassLocation}-${glass.glassName}`.replace(" ", "-")] = savedPart;
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
:answers="styles"
|
||||
groupName="ChooseVehicleStyle"
|
||||
textPosition="text-start"
|
||||
v-model="selectedValueAsArray"
|
||||
v-model="selectedValue"
|
||||
isRequired=true
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -34,14 +34,13 @@ export default {
|
|||
questionText(){
|
||||
return this.getCmsContent(this.cmsWidgetName, 'QuestionText');
|
||||
},
|
||||
selectedValueAsArray: {
|
||||
selectedValue: {
|
||||
get: function() {
|
||||
const modelValueAsArray = this.modelValue ? [this.modelValue] : [];
|
||||
return modelValueAsArray;
|
||||
},
|
||||
set: function(newValue) {
|
||||
const newValueAsScalar = newValue && newValue.length > 0 ? newValue[0] : null;
|
||||
this.$emit("update:modelValue", newValueAsScalar);
|
||||
this.$emit("update:modelValue", newValue);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
:answers="years"
|
||||
groupName="ChooseVehicleYear"
|
||||
textPosition="text-start"
|
||||
v-model="selectedValueAsArray"
|
||||
v-model="selectedValue"
|
||||
isRequired=true
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -35,14 +35,13 @@ export default {
|
|||
questionText(){
|
||||
return this.getCmsContent(this.cmsWidgetName, 'QuestionText');
|
||||
},
|
||||
selectedValueAsArray: {
|
||||
selectedValue: {
|
||||
get: function() {
|
||||
const modelValueAsArray = this.modelValue ? [this.modelValue] : [];
|
||||
return modelValueAsArray;
|
||||
},
|
||||
set: function(newValue) {
|
||||
const newValueAsScalar = newValue && newValue.length > 0 ? newValue[0] : null;
|
||||
this.$emit("update:modelValue", newValueAsScalar);
|
||||
this.$emit("update:modelValue", newValue);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -183,15 +183,10 @@ export default {
|
|||
previouslyEnteredCarId: '',
|
||||
invalidZip: '',
|
||||
vinPopulatedOnPageLoad: this.getVinFromStore()?.length > 0,
|
||||
isInsuranceVerified: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.attachCustomEvents();
|
||||
if (this.vinPopulatedOnPageLoad) {
|
||||
this.setupVinMask();
|
||||
this.isInsuranceVerified = store.getters.payment.insuranceCoverage.isVerified || getFunnelCookie().HasDelayedClaimRegistration;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
vin() {
|
||||
|
|
@ -239,12 +234,20 @@ export default {
|
|||
return this.getCmsContent("PerfectMatchInsuranceVerifiedAlert", "BodyText").replaceAll("{custom:damage}",
|
||||
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: {
|
||||
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() {
|
||||
return store.getters.vehicle.carId !== null;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ const fmgPageValues = {
|
|||
REVEAL: "reveal",
|
||||
ESTIMATE: "estimate",
|
||||
ADDRESS_VEHICLES: "address-vehicles",
|
||||
QUOTE: "quote"
|
||||
};
|
||||
|
||||
export { fmgPageValues };
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ html {
|
|||
input[type=radio]+label:before,
|
||||
input[type=checkbox]+label:before {
|
||||
border: 1px solid $red;
|
||||
background-color: initial;
|
||||
}
|
||||
input[type=checkbox]:checked + label:before {
|
||||
border: 1px solid $blue;
|
||||
|
|
|
|||
|
|
@ -223,18 +223,26 @@ export default {
|
|||
span {
|
||||
font-size: .875rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.col {
|
||||
&:first-of-type {
|
||||
label {
|
||||
border-bottom-left-radius: 0.5rem;
|
||||
border-top-left-radius: 0.5rem;
|
||||
z-index: 2;
|
||||
.list-button-horizontal {
|
||||
label {
|
||||
border-bottom-left-radius: 0.5rem;
|
||||
border-top-left-radius: 0.5rem;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
label {
|
||||
border-bottom-right-radius: 0.5rem;
|
||||
border-top-right-radius: 0.5rem;
|
||||
.list-button-horizontal {
|
||||
label {
|
||||
border-bottom-right-radius: 0.5rem;
|
||||
border-top-right-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@ describe("list-card.vue", () => {
|
|||
});
|
||||
wrapper.vm.handleCheckChange();
|
||||
// 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 () => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
<template>
|
||||
<div :class="'col' + colLength">
|
||||
<div :class="{'h-100': !isWide}">
|
||||
<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="[
|
||||
'h-100',
|
||||
isWide ? 'horizontal' : '',
|
||||
(errors.length > 0 || hasError) ? 'has-error' : '',
|
||||
]"
|
||||
|
|
@ -19,7 +20,8 @@
|
|||
:value="value"
|
||||
:aria-required="isRequired"
|
||||
v-model="checkValue"
|
||||
@change="handleInputChange()"
|
||||
:checked="checkValue"
|
||||
@change="handleInputChange"
|
||||
/>
|
||||
<label
|
||||
tabindex="-1"
|
||||
|
|
@ -27,7 +29,7 @@
|
|||
:aria-labelledby="buttonID"
|
||||
class="d-flex w-100 align-items-center px-2 h-100"
|
||||
:class="getLabelClasses"
|
||||
@mouseup="triggerButton()"
|
||||
@mouseup="triggerButton"
|
||||
>
|
||||
<img
|
||||
:id="buttonImageId"
|
||||
|
|
@ -93,7 +95,7 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
checkValue: [Boolean, String],
|
||||
checkValue: null,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
|
@ -102,6 +104,14 @@ export default {
|
|||
? this.selectedValues.includes(this.value)
|
||||
: 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
|
||||
if (this.clearOnUnmount) {
|
||||
|
|
@ -164,6 +174,15 @@ export default {
|
|||
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) {
|
||||
const inputType = props.isMultiSelect ? "checkbox" : "radio";
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ describe("radio.vue", () => {
|
|||
isRequired: true,
|
||||
modelValue: ["List Card Checkbox"],
|
||||
value: "Car-Front",
|
||||
selectedValues: ["Car-Front"]
|
||||
selectedValues: "Car-Front"
|
||||
},
|
||||
});
|
||||
// Assert
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
<template>
|
||||
<!-- 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
|
||||
type="radio"
|
||||
class="form-check-input"
|
||||
aria-checked="false"
|
||||
:name="groupName"
|
||||
:id="buttonID"
|
||||
:aria-required="isRequired"
|
||||
:value="value"
|
||||
:v-model="checkValue"
|
||||
@change="handleCheckChange()"
|
||||
:checked="checkValue"
|
||||
/>
|
||||
type="radio"
|
||||
class="form-check-input"
|
||||
aria-checked="false"
|
||||
:name="groupName"
|
||||
:id="buttonID"
|
||||
:aria-required="isRequired"
|
||||
:value="value"
|
||||
:v-model="checkValue"
|
||||
@change="handleCheckChange"
|
||||
:checked="checkValue"
|
||||
:validationRules="validationRules"
|
||||
/>
|
||||
<label class="d-flex align-items-start form-check-label" :for="buttonID">
|
||||
<p v-if="buttonLabel" class="m-0">{{ buttonLabel }}</p>
|
||||
<span v-if="screenReaderOnlyText" class="sr-only">{{
|
||||
|
|
@ -37,8 +38,9 @@ export default {
|
|||
default: "",
|
||||
},
|
||||
screenReaderOnlyText: String,
|
||||
selectedValues: [Array, String],
|
||||
selectedValues: String,
|
||||
hasError: Boolean,
|
||||
validationRules: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -47,16 +49,15 @@ export default {
|
|||
},
|
||||
created() {
|
||||
if (this.selectedValues) {
|
||||
this.checkValue = this.selectedValues[0] === this.value;
|
||||
}else{
|
||||
this.checkValue = this.selectedValues === this.value;
|
||||
this.handleCheckChange();
|
||||
} else{
|
||||
this.checkValue = false;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClick(value) {
|
||||
this.handleChange(value);
|
||||
},
|
||||
handleCheckChange() {
|
||||
this.handleChange(this.value);
|
||||
const emitEvent = {
|
||||
checkValue: this.checkValue,
|
||||
value: this.value.toString(),
|
||||
|
|
@ -88,13 +89,17 @@ export default {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.form-check {
|
||||
position: relative;
|
||||
|
||||
.form-check-input {
|
||||
border: 1px solid $gray-500;
|
||||
border-radius: 100%;
|
||||
border-radius: 50%;
|
||||
margin-right: 0.5rem;
|
||||
|
||||
&:checked {
|
||||
background-color: $white;
|
||||
background-size: 71%;
|
||||
background-position: center;
|
||||
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");
|
||||
+ label {
|
||||
|
|
@ -108,7 +113,15 @@ export default {
|
|||
&:focus {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
}
|
||||
|
||||
&, & + label {
|
||||
margin-top: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.form-check-input {
|
||||
box-shadow: 0 0 0 4px $blue-300;
|
||||
|
|
|
|||
Loading…
Reference in a new issue