CSR-1384: add validation on modal when multiple wiper options
This commit is contained in:
parent
f8811c6984
commit
bbf4b083e8
4 changed files with 271 additions and 65 deletions
|
|
@ -92,6 +92,7 @@ import listButton from "@/ux-components/list-button/list-button";
|
|||
import listButtonHorizontal from "@/ux-components/list-button-horizontal/list-button-horizontal";
|
||||
import listCard from "@/ux-components/list-card/list-card";
|
||||
import radio from "@/ux-components/radio/radio";
|
||||
import checkbox from "@/ux-components/checkbox/checkbox";
|
||||
import { useField, ErrorMessage } from "vee-validate";
|
||||
import { queryStrings } from "@/constants/query-strings";
|
||||
|
||||
|
|
@ -209,6 +210,9 @@ export default {
|
|||
case "radio":
|
||||
classes = "ui-radio d-flex";
|
||||
break;
|
||||
case "checkbox":
|
||||
classes = "ui-checkbox d-flex";
|
||||
break;
|
||||
case "servicePackageRadio":
|
||||
classes = "package-main";
|
||||
break;
|
||||
|
|
@ -317,6 +321,7 @@ export default {
|
|||
listCard,
|
||||
ErrorMessage,
|
||||
radio,
|
||||
checkbox,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -26,30 +26,24 @@
|
|||
v-if="wipersModal"
|
||||
@footer-button-action="addWipersToCart('WipersModal', wipersModal)"
|
||||
footerButtonActionName="footer-button-action">
|
||||
<div v-if="wipersToDisplay === wipersToDisplayStrings.BOTH" class="wiper-options">
|
||||
<div>
|
||||
<checkboxQuestion
|
||||
class="mb-5"
|
||||
cmsWidgetName="AddFrontBladesQuestionWidget"
|
||||
v-model="selectedFrontWipers" />
|
||||
<p class="price">+${{ wipersModal.totalFrontPrice }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<checkboxQuestion
|
||||
class="mb-5"
|
||||
cmsWidgetName="AddRearBladesQuestionWidget"
|
||||
v-model="selectedRearWipers" />
|
||||
<p class="price">+${{ wipersModal.totalRearPrice }}</p>
|
||||
</div>
|
||||
<div v-if="wipersOffered === wipersOfferedStrings.BOTH" class="wiper-options">
|
||||
<buttonQuestion
|
||||
ref="buttonQuestion"
|
||||
buttonTypeString="checkbox"
|
||||
class="mt-5"
|
||||
:answers="wipersOptions"
|
||||
groupName="wipers-options"
|
||||
textPosition="text-center"
|
||||
v-model="selectedWipers"
|
||||
isMultiSelect
|
||||
isRequired
|
||||
validationRules="checkbox-required"
|
||||
:additionalButtonData="additionalButtonData" />
|
||||
</div>
|
||||
<div
|
||||
v-else-if="wipersToDisplay === wipersToDisplayStrings.FRONT"
|
||||
class="wiper-options">
|
||||
<div v-else-if="wipersOffered === wipersOfferedStrings.FRONT" class="wiper-options">
|
||||
<p class="price">+${{ wipersModal.totalFrontPrice }}</p>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="wipersToDisplay === wipersToDisplayStrings.REAR"
|
||||
class="wiper-options">
|
||||
<div v-else-if="wipersOffered === wipersOfferedStrings.REAR" class="wiper-options">
|
||||
<p class="price">+${{ wipersModal.totalRearPrice }}</p>
|
||||
</div>
|
||||
</contentGroupModal>
|
||||
|
|
@ -62,12 +56,17 @@
|
|||
import buttonQuestion from "@/digital-components/button-question/button-question";
|
||||
import addVapsButton from "./add-vaps-button/add-vaps-button";
|
||||
import contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal";
|
||||
import checkboxQuestion from "@/digital-components/checkbox-question/checkbox-question";
|
||||
|
||||
// SUPPORTING
|
||||
import { partTypeStrings } from "@/constants/part-type-strings";
|
||||
import { defineRule } from "vee-validate";
|
||||
import { required } from "@/helpers/validation-rules";
|
||||
import { errorMessages } from "@/constants/error-messages";
|
||||
|
||||
const wipersToDisplayStrings = {
|
||||
// DEFINE VALIDATION RULES
|
||||
defineRule("checkbox-required", required(errorMessages.OPTION_REQUIRED));
|
||||
|
||||
const wipersOfferedStrings = {
|
||||
FRONT: "FRONT",
|
||||
REAR: "REAR",
|
||||
BOTH: "BOTH",
|
||||
|
|
@ -83,15 +82,22 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
addVapsButton: addVapsButton,
|
||||
selectedFrontWipers: false,
|
||||
selectedRearWipers: false,
|
||||
wipersToDisplay: null,
|
||||
wipersToAddToCart: [],
|
||||
wipersOffered: null,
|
||||
isModalOpened: false,
|
||||
selectedWipers: this.getSelectedWipers(),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
openModal(modalName) {
|
||||
this.$refs[modalName].openModal();
|
||||
},
|
||||
closeModal(modalName) {
|
||||
this.$refs[modalName].closeModal();
|
||||
},
|
||||
formatString(str) {
|
||||
return String(str).replaceAll(" ", "-");
|
||||
},
|
||||
addRainDefenseToCart(modalName, part) {
|
||||
const alreadyInCart = this.currentCartItems.vaps.some((item) => {
|
||||
return item.partType === part.partType;
|
||||
|
|
@ -99,9 +105,11 @@ export default {
|
|||
if (!alreadyInCart) {
|
||||
this.currentCartItems.vaps.push(part);
|
||||
}
|
||||
this.$refs[modalName].closeModal();
|
||||
this.closeModal(modalName);
|
||||
},
|
||||
addWipersToCart(modalName, part) {
|
||||
const itemsForCart = [];
|
||||
|
||||
const frontWipersAlreadyInCart = this.currentCartItems.vaps.some((item) => {
|
||||
return item.partType === part.frontWiperLineItems[0].partType;
|
||||
});
|
||||
|
|
@ -109,36 +117,43 @@ export default {
|
|||
return item.partType === part.rearWiperLineItems[0]?.partType;
|
||||
});
|
||||
|
||||
if (this.wipersToDisplay === wipersToDisplayStrings.FRONT) {
|
||||
this.selectedFrontWipers = true; // set as true if front wipers is only option
|
||||
if (this.wipersOffered === wipersOfferedStrings.BOTH) {
|
||||
this.selectedWipers.forEach((wiper) => {
|
||||
itemsForCart.push(wiper);
|
||||
});
|
||||
} else {
|
||||
if (
|
||||
!frontWipersAlreadyInCart &&
|
||||
this.wipersOffered === wipersOfferedStrings.FRONT
|
||||
) {
|
||||
itemsForCart.push(wipersOfferedStrings.FRONT);
|
||||
}
|
||||
if (!rearWipersAlreadyInCart && this.wipersOffered === wipersOfferedStrings.REAR) {
|
||||
itemsForCart.push(wipersOfferedStrings.REAR);
|
||||
}
|
||||
}
|
||||
if (this.wipersToDisplay === wipersToDisplayStrings.REAR) {
|
||||
this.selectedRearWipers = true; // set as true if rear wipers is only option
|
||||
}
|
||||
if (!frontWipersAlreadyInCart && this.selectedFrontWipers) {
|
||||
if (part.frontWiperLineItems) {
|
||||
// for wipers, a "combined" part (frontWiperLineItems)
|
||||
|
||||
itemsForCart.forEach((type) => {
|
||||
if (type === wipersOfferedStrings.FRONT) {
|
||||
part.frontWiperLineItems.forEach((part) => {
|
||||
this.currentCartItems.vaps.push(part);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!rearWipersAlreadyInCart && this.selectedRearWipers) {
|
||||
if (part.rearWiperLineItems) {
|
||||
if (type === wipersOfferedStrings.REAR) {
|
||||
part.rearWiperLineItems.forEach((part) => {
|
||||
this.currentCartItems.vaps.push(part);
|
||||
});
|
||||
}
|
||||
}
|
||||
this.$refs[modalName].closeModal();
|
||||
});
|
||||
|
||||
if (this.wipersToDisplay === wipersToDisplayStrings.BOTH) {
|
||||
this.selectedFrontWipers = false; // set as false if both options are given
|
||||
this.selectedRearWipers = false; // set as false if both options are given
|
||||
}
|
||||
this.closeModal(modalName);
|
||||
this.selectedWipers;
|
||||
},
|
||||
updateWipersToDisplay(value) {
|
||||
this.wipersToDisplay = value;
|
||||
updateWipersOffered(value) {
|
||||
this.wipersOffered = value;
|
||||
},
|
||||
getSelectedWipers() {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -229,31 +244,72 @@ export default {
|
|||
];
|
||||
prices.sort((a, b) => a - b);
|
||||
modalData.SubText = "+$" + prices[0] + " - $" + prices[prices.length - 1];
|
||||
this.updateWipersToDisplay(wipersToDisplayStrings.BOTH);
|
||||
this.updateWipersOffered(wipersOfferedStrings.BOTH);
|
||||
buttons.push(modalData);
|
||||
} else {
|
||||
// NO FRONT WIPERS IN CART; SHOW ONLY FRONT WIPERS BUTTON
|
||||
modalData.SubText = "+$" + this.wipersModal?.totalFrontPrice;
|
||||
this.updateWipersToDisplay(wipersToDisplayStrings.FRONT);
|
||||
this.updateWipersOffered(wipersOfferedStrings.FRONT);
|
||||
buttons.push(modalData);
|
||||
}
|
||||
} else if (!rearWipersInCart && this.wipersModal?.rearWiperLineItems.length > 0) {
|
||||
// NO REAR WIPERS IN CART; SHOW ONLY REAR WIPERS BUTTON
|
||||
modalData.SubText = "+$" + this.wipersModal?.totalRearPrice;
|
||||
this.updateWipersToDisplay(wipersToDisplayStrings.REAR);
|
||||
this.updateWipersOffered(wipersOfferedStrings.REAR);
|
||||
buttons.push(modalData);
|
||||
}
|
||||
|
||||
return buttons;
|
||||
},
|
||||
wipersToDisplayStrings() {
|
||||
return wipersToDisplayStrings;
|
||||
wipersOfferedStrings() {
|
||||
return wipersOfferedStrings;
|
||||
},
|
||||
wipersOptions() {
|
||||
const wipersOptions = [];
|
||||
const wipersFrontCopy = this.getCmsContent(
|
||||
"AddFrontBladesQuestionWidget",
|
||||
"QuestionText"
|
||||
);
|
||||
const wipersRearCopy = this.getCmsContent(
|
||||
"AddRearBladesQuestionWidget",
|
||||
"QuestionText"
|
||||
);
|
||||
const frontWipersLineItems = this.wipersModal.frontWiperLineItems;
|
||||
const rearWipersLineItems = this.wipersModal.rearWiperLineItems;
|
||||
|
||||
if (frontWipersLineItems) {
|
||||
const wipersOption = {
|
||||
buttonLabel: wipersFrontCopy,
|
||||
groupName: "wipers-options",
|
||||
value: wipersOfferedStrings.FRONT,
|
||||
buttonLabelSubCopy: "buttonLabelSubCopy",
|
||||
buttonBodyCopy: "buttonBodyCopy",
|
||||
buttonAuxillaryCopy: "buttonAuxillaryCopy",
|
||||
buttonFooterCopy: "buttonFooterCopy",
|
||||
additionalButtonData: "+$" + this.wipersModal.totalFrontPrice,
|
||||
};
|
||||
wipersOptions.push(wipersOption);
|
||||
}
|
||||
if (rearWipersLineItems) {
|
||||
const wipersOption = {
|
||||
buttonLabel: wipersRearCopy,
|
||||
groupName: "wipers-options",
|
||||
value: wipersOfferedStrings.REAR,
|
||||
buttonLabelSubCopy: "buttonLabelSubCopy",
|
||||
buttonBodyCopy: "buttonBodyCopy",
|
||||
buttonAuxillaryCopy: "buttonAuxillaryCopy",
|
||||
buttonFooterCopy: "buttonFooterCopy",
|
||||
additionalButtonData: "+$" + this.wipersModal.totalRearPrice,
|
||||
};
|
||||
wipersOptions.push(wipersOption);
|
||||
}
|
||||
|
||||
return wipersOptions;
|
||||
},
|
||||
},
|
||||
components: {
|
||||
buttonQuestion,
|
||||
contentGroupModal,
|
||||
checkboxQuestion,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -286,26 +342,31 @@ export default {
|
|||
text-align: center;
|
||||
}
|
||||
.wiper-options {
|
||||
& > div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
.ui-checkbox {
|
||||
display: block;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
& > div.col {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.checkbox {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.form-check {
|
||||
margin-bottom: 0;
|
||||
.additional-button-data {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.75;
|
||||
padding-left: 0.5rem;
|
||||
color: $green;
|
||||
}
|
||||
}
|
||||
}
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
p.price {
|
||||
font-size: 0.875rem;
|
||||
padding-left: 0.5rem;
|
||||
color: $green;
|
||||
line-height: 1.75;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
66
src/ux-components/checkbox/checkbox.spec.js
Normal file
66
src/ux-components/checkbox/checkbox.spec.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { mount } from "@vue/test-utils";
|
||||
import checkbox from "./checkbox";
|
||||
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
|
||||
|
||||
describe("checkbox.vue", () => {
|
||||
it("Should have correct group name", async () => {
|
||||
// Arrange
|
||||
let { wrapper } = setupMocks({
|
||||
mountOptionsMockData: {
|
||||
propsData: {
|
||||
groupName: "checkbox-button-test",
|
||||
value: "test value",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Act
|
||||
const input = wrapper.find("input");
|
||||
|
||||
// Assert
|
||||
expect(input.attributes().name).toEqual("checkbox-button-test");
|
||||
});
|
||||
|
||||
it("Should have correct label text", async () => {
|
||||
// Act
|
||||
let { wrapper } = setupMocks({
|
||||
mountOptionsMockData: {
|
||||
propsData: {
|
||||
buttonLabel: "label text",
|
||||
value: "test value",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Arrange
|
||||
const paragraph = wrapper.find("p");
|
||||
|
||||
// Assert
|
||||
expect(paragraph.text()).toEqual("label text");
|
||||
});
|
||||
|
||||
it("Should have correct screenreader-only text", async () => {
|
||||
// Act
|
||||
let { wrapper } = setupMocks({});
|
||||
|
||||
// Arrange
|
||||
await wrapper.setProps({
|
||||
screenReaderOnlyText: "screenreader text",
|
||||
value: "test value",
|
||||
});
|
||||
const paragraph = wrapper.find(".sr-only");
|
||||
|
||||
// Assert
|
||||
expect(paragraph.text()).toEqual("screenreader text");
|
||||
});
|
||||
});
|
||||
|
||||
function setupMocks({ mountOptionsMockData = {} }) {
|
||||
const wrapper = mount(checkbox, {
|
||||
...mountOptionsMockData,
|
||||
mixins: [inputButtonWrapperMixin],
|
||||
});
|
||||
|
||||
return { wrapper };
|
||||
}
|
||||
74
src/ux-components/checkbox/checkbox.vue
Normal file
74
src/ux-components/checkbox/checkbox.vue
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<template>
|
||||
<baseInputButton
|
||||
v-bind="$props"
|
||||
buttonWrapperClasses="form-check base-input-button checkbox"
|
||||
inputClasses="form-check-input"
|
||||
v-model="selectedValue">
|
||||
<div class="d-flex align-items-start form-check-label">
|
||||
<p v-if="buttonLabel" class="m-0">{{ buttonLabel }}</p>
|
||||
<span v-if="screenReaderOnlyText" class="sr-only">{{ screenReaderOnlyText }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="additionalButtonData"
|
||||
v-html="additionalButtonData"
|
||||
class="additional-button-data"></div>
|
||||
</baseInputButton>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import baseInputButton from "@/digital-components/base-input-button/base-input-button";
|
||||
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
|
||||
|
||||
export default {
|
||||
name: "checkbox",
|
||||
mixins: [inputButtonWrapperMixin],
|
||||
components: {
|
||||
baseInputButton,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.form-check {
|
||||
position: relative;
|
||||
|
||||
.form-check-input {
|
||||
border: 1px solid $gray-500; //
|
||||
border-radius: 2px; //
|
||||
margin-right: 0.5rem;
|
||||
opacity: 1;
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
|
||||
&:checked {
|
||||
background-color: $blue; //
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
border: 1px solid $blue;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e");
|
||||
|
||||
+ .form-check-label {
|
||||
p {
|
||||
font-weight: 400;
|
||||
font-size: 0.875rem;
|
||||
color: $gray-600;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.form-check-input:not(:checked) {
|
||||
box-shadow: 0 0 0 4px $blue-300;
|
||||
}
|
||||
}
|
||||
p {
|
||||
font-weight: 400;
|
||||
font-size: 0.875rem;
|
||||
color: $gray-600;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in a new issue