Merge pull request #569 from Safelite/CSR-465-add-validation-to-parts-page

Csr 465 add validation to parts page
This commit is contained in:
bmauger 2022-06-16 16:30:24 -04:00 committed by GitHub
commit c8dfc1696a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 198 additions and 169 deletions

View file

@ -29,7 +29,11 @@
:buttonID="`${glassLocation}-${glassName}-${name}`" :buttonID="`${glassLocation}-${glassName}-${name}`"
:groupName="`${glassLocation}-${glassName}`" :groupName="`${glassLocation}-${glassName}`"
@isCheckedChanged="ResetTintAndPartSelections()" @isCheckedChanged="ResetTintAndPartSelections()"
:validationRules="validationRules"
/> />
<div class="row form-test-error mt-1">
<error-message :name="`${glassLocation}-${glassName}`" v-if="!suppressError"></error-message>
</div>
</div> </div>
</div> </div>
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
@ -53,6 +57,7 @@
:loaderEnabled="false" :loaderEnabled="false"
isRequired isRequired
:groupName="`${glassLocation}-${glassName}-${name}`" :groupName="`${glassLocation}-${glassName}-${name}`"
:validationRules="validationRules"
/> />
</div> </div>
</div> </div>
@ -68,6 +73,7 @@ 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';
export default { export default {
name: "glass-part-question", name: "glass-part-question",
@ -84,6 +90,7 @@ export default {
glassLocation: String, glassLocation: String,
colorAnswers: Array, colorAnswers: Array,
modelValue: Object, modelValue: Object,
validationRules: String,
}, },
mounted() { mounted() {
this.LoadPreselectedValues(); this.LoadPreselectedValues();
@ -91,6 +98,7 @@ export default {
components: { components: {
listCard, listCard,
buttonQuestion, buttonQuestion,
ErrorMessage,
}, },
computed: { computed: {
colorQuestionText() { colorQuestionText() {

View file

@ -1,5 +1,11 @@
<template> <template>
<div class="page-container-grouped-styles vehicle-parts"> <Form
@submit="onSubmit"
@invalid-submit="onInvalidSubmit"
ref="theForm"
v-slot="{ meta }"
>
<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" />
@ -8,10 +14,10 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<alert <alert
class="rounded border-0 shadow-sm" class="rounded border-0 shadow-sm"
alertClass="alert-warning" alertClass="alert-warning"
cmsWidgetName="AlertWidget" cmsWidgetName="AlertWidget"
:isDismissible="false" :isDismissible="false"
/> />
</div> </div>
</div> </div>
@ -24,16 +30,24 @@
</div> </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"
/> />
</div> </div>
<funnelFooter cmsWidgetName="FunnelFooterWidget" ref="funnelFooter" @back-clicked="backButtonAction" @ForwardClicked="forwardButtonAction" /> <funnelFooter
cmsWidgetName="FunnelFooterWidget"
ref="funnelFooter"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction"
/>
</div> </div>
</div> </div>
</Form>
</template> </template>
<script> <script>
@ -50,170 +64,177 @@ 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 { 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",
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
// Call APIs // Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
// Settle promises and get results // Settle promises and get results
const promiseResultMap = [{ const promiseResultMap = [{
resultKey: "cmsContent", resultKey: "cmsContent",
promise: cmsContentPromise, promise: cmsContentPromise,
}, ]; }, ];
const resultMap = await settleAllPromises(promiseResultMap); const resultMap = await settleAllPromises(promiseResultMap);
// Call the "next" function to complete the transition to this page. // Call the "next" function to complete the transition to this page.
next((vm) => { next((vm) => {
vm.setCmsContent(resultMap.cmsContent); vm.setCmsContent(resultMap.cmsContent);
// Glass Part Question dynamic component // Glass Part Question dynamic component
Object.keys(vm.$refs) Object.keys(vm.$refs)
.filter((r) => r.includes(vm.RefPrefix) && vm.$refs[r][0] !== undefined) .filter((r) => r.includes(vm.RefPrefix) && vm.$refs[r][0] !== undefined)
.forEach((c) => .forEach((c) =>
vm.$refs[c][0].initializeComponent({ vm.$refs[c][0].initializeComponent({
ColorQuestionWidget: resultMap.cmsContent.ColorQuestionWidget, ColorQuestionWidget: resultMap.cmsContent.ColorQuestionWidget,
FeatureQuestionWidget: resultMap.cmsContent.FeatureQuestionWidget, FeatureQuestionWidget: resultMap.cmsContent.FeatureQuestionWidget,
}) })
); );
}); });
}, },
data() { data() {
return { return {
glassParts: {}, glassParts: {},
alertWidgetData: Object, alertWidgetData: Object,
}; };
}, },
components: { components: {
glassPartQuestion, Form,
funnelHeader, glassPartQuestion,
vehicleBanner, funnelHeader,
funnelSubHeader, vehicleBanner,
funnelFooter, funnelSubHeader,
alert, funnelFooter,
}, alert,
computed: { },
PartsForQuestions() { computed: {
const partsData = this.PartsFromApi; PartsForQuestions() {
const partsData = this.PartsFromApi;
// Map API result data, to vehicle-parts data structure // Map API result data, to vehicle-parts data structure
const mappedData = partsData.partsOrQuestions.map((g) => { const mappedData = partsData.partsOrQuestions.map((g) => {
return { return {
glassName: g.glassName, glassName: g.glassName,
glassLocation: g.glassLocation, glassLocation: g.glassLocation,
colorAnswers: g.parts.reduce((arr, p) => { colorAnswers: g.parts.reduce((arr, p) => {
arr.push({ arr.push({
ColorAnswerText: p.color, ColorAnswerText: p.color,
FeatureAnswers: [ FeatureAnswers: [
{ {
FeatureAnswerText: FeatureAnswerText:
p.description === "" ? p.color : p.description, p.description === "" ? p.color : p.description,
PartNumber: p.partNumber, PartNumber: p.partNumber,
}, },
], ],
});
return arr;
}, []),
};
});
return mappedData;
},
PartsFromApi() {
return store.getters.pageData(fmgPageValues.VEHICLE_PARTS);
},
RefPrefix() {
return "partQuestion";
},
},
methods: {
arePagePrerequisitesValid() {
// Check if isRepair is populated and if the pageData we need is here (Parts data)
if (
(store.getters.damage.isRepair != null) &&
Object.keys(store.getters.pageData(fmgPageValues.VEHICLE_PARTS))
.length !== 0
) {
return true;
}
return false;
},
backButtonAction() {
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
},
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) {
throw new Error("Could not match any parts to the selected parts");
}
// Save parts to the store.
store.commit(storeMutations.UPDATE_GLASS_PARTS, matchedParts);
// Navigate to the next page.
this.$router.navigateAfterSave(
this.navigationScenarios.SELECTED_PARTS,
this.$route
);
},
resetDependentState() {
// Nothing additional to reset here: The page save is already fully resetting all the line-items on the order
},
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],
};
}
}); });
return arr;
}, []),
};
});
return mappedData;
},
PartsFromApi() {
return store.getters.pageData(fmgPageValues.VEHICLE_PARTS);
},
RefPrefix() {
return "partQuestion";
},
},
methods: {
arePagePrerequisitesValid() {
// Check if isRepair is populated and if the pageData we need is here (Parts data)
if (
(store.getters.damage.isRepair != null) &&
Object.keys(store.getters.pageData(fmgPageValues.VEHICLE_PARTS))
.length !== 0
) {
return true;
}
return false;
},
backButtonAction() {
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
},
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) {
throw new Error("Could not match any parts to the selected parts");
}
// Save parts to the store.
store.commit(storeMutations.UPDATE_GLASS_PARTS, matchedParts);
// Navigate to the next page.
this.$router.navigateAfterSave(
this.navigationScenarios.SELECTED_PARTS,
this.$route
);
},
resetDependentState() {
// Nothing additional to reset here: The page save is already fully resetting all the line-items on the order
},
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],
};
}
}); });
}); });
}, });
},
mounted() {
this.LoadInitialPartsData();
}, },
},
mounted() {
this.LoadInitialPartsData();
},
}; };
</script> </script>