ga event defect

This commit is contained in:
Donielle Austin 2022-05-18 18:07:34 -04:00
commit 7fd053e043
39 changed files with 1157 additions and 273 deletions

View file

@ -127,4 +127,45 @@ stages:
indexDeployVariables: indexDeployVariables:
__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__: $(__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__) __VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__: $(__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__)
__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__: $(__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__) __VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__: $(__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__)
cfDistributionId: $(cfDistributionId)
# Prod Build/Deploy
- stage: Prod
condition: eq(variables['Build.SourceBranch'], variables['prod-branch'] )
variables:
- group: FixMyGlassProd
jobs:
- deployment: prodBuildDeployment
displayName: Build and Deploy FMG - Prod
environment: digitalCloud-prod
container: node
workspace:
clean: all
strategy:
runOnce:
deploy:
steps:
- checkout: self
clean: true
- template: templates/digital/step-build-vue.yml@AzureDevOps
parameters:
buildOutputDir: dist
- template: templates/digital/step-deploy-vue.yml@AzureDevOps
parameters:
artifactName: vueDist
awsProfile: $(prodDeploymentProfile)
outputPath: /fmg/
deployBuckets:
safelite-prod-fmg-us-east-1:
clearFolder: true
deployFolder: ''
region: us-east-1
appDeployVariables:
__VUE_APP_CONSUMER_CF_DISTRO__: $(__VUE_APP_CONSUMER_CF_DISTRO__)
__VUE_APP_GOOGLE_PLACES_API_KEY__: $(__VUE_APP_GOOGLE_PLACES_API_KEY__)
__VUE_APP_HERITAGE_FUNNEL__: $(__VUE_APP_HERITAGE_FUNNEL__)
__VUE_APP_CURRENT_ENVIRONMENT__: $(__VUE_APP_CURRENT_ENVIRONMENT__)
indexDeployVariables:
__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__: $(__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__)
__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__: $(__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__)
cfDistributionId: $(cfDistributionId) cfDistributionId: $(cfDistributionId)

View file

@ -19,15 +19,18 @@ module.exports = {
"!src/layouts/part-questions/**/*.vue", "!src/layouts/part-questions/**/*.vue",
"!src/layouts/reveal/**/*.vue", "!src/layouts/reveal/**/*.vue",
"!src/layouts/estimate/**/*.vue", "!src/layouts/estimate/**/*.vue",
// REMOVE THESE AFTER WRITING UNIT TESTS // TODO REMOVE THESE AFTER WRITING UNIT TESTS
"!src/layouts/address-lookup/address-lookup.vue", "!src/layouts/address-lookup/address-lookup.vue",
"!src/layouts/address-lookup/customer-questions/customer-questions.vue", "!src/layouts/address-lookup/customer-questions/customer-questions.vue",
"!src/layouts/address-lookup/customer-questions/address-questions/address-questions.vue", "!src/layouts/address-lookup/customer-questions/address-questions/address-questions.vue",
"!src/layouts/address-vehicles/address-vehicles.vue",
"!src/layouts/address-vehicles/address-vehicles-question/address-vehicles-question.vue",
"!src/common-components/dropdown-question/dropdown-question.vue", "!src/common-components/dropdown-question/dropdown-question.vue",
"!src/common-components/textbox-question/textbox-question.vue", "!src/common-components/textbox-question/textbox-question.vue",
"!src/ux-components/alert\alert.vue",
"!src/helpers/validation-rules.js", "!src/helpers/validation-rules.js",
// END // END
], //! means exclude from coverage. ], // ! means exclude from coverage.
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"], testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
coverageThreshold: { coverageThreshold: {
global: { global: {

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 30 KiB

View file

@ -1,12 +1,12 @@
<!-- Documented in confluence https://safelite.atlassian.net/wiki/spaces/DC/pages/76644418/Button+Question+Component --> <!-- Documented in confluence https://safelite.atlassian.net/wiki/spaces/DC/pages/76644418/Button+Question+Component -->
<template> <template>
<div :class="isOverflowScrollable ? 'button-question button-question-overflow' : 'button-question'"> <div :class="isOverflowScrollable ? 'button-question button-question-overflow' : 'button-question'">
<div v-if="questionText" :class="[this.buttonType === 'radio' ? ['mt-2', 'mb-0'] : ['mt-5', 'mb-4'], 'd-flex']"> <div v-if="questionText" class="question-text d-flex">
<span class="fs-5 fw-bold w-100" :class="this.buttonType === 'radio' ? 'text-start' : 'text-center'">{{ questionText }}</span> <span class="fs-5 fw-bold w-100">{{ questionText }}</span>
</div> </div>
<div class="w-100 d-flex justify-content-center"> <div class="w-100 d-flex justify-content-center">
<fieldset class="w-100" :aria-required=isRequired :class="getFieldSetClasses" :role="isMultiSelect ? 'group' : 'radiogroup'" :aria-labelledby="groupName ? groupName + '-radio-group' : ''"> <fieldset class="w-100" :aria-required=isRequired :class="getFieldSetClasses" :role="isMultiSelect ? 'group' : 'radiogroup'" :aria-labelledby="groupName">
<legend class="sr-only" :data-focus-target="groupName" tabindex="-1"> <legend class="sr-only" :data-focus-target="groupName" :id="groupName" tabindex="-1">
{{(isMultiSelect && answers && answers.length > 1) ? 'Select one or more options below.' : 'Select an option below.' }} {{(isMultiSelect && answers && answers.length > 1) ? 'Select one or more options below.' : 'Select an option below.' }}
</legend> </legend>
<div :class="getComponentWrapperClasses"> <div :class="getComponentWrapperClasses">
@ -40,7 +40,7 @@
</div> </div>
</fieldset> </fieldset>
</div> </div>
<div class="row mt-2 form-test-error"> <div class="row form-test-error">
<error-message :name="groupName" v-if="!suppressError"></error-message> <error-message :name="groupName" v-if="!suppressError"></error-message>
</div> </div>
</div> </div>
@ -160,7 +160,7 @@ export default {
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss">
.button-question-overflow { .button-question-overflow {
height: calc(100vh - 274px); height: calc(100vh - 274px);
@ -174,4 +174,12 @@ export default {
.button-question { .button-question {
color: $black; color: $black;
} }
.question-text {
margin-top: 1.5rem;
margin-bottom: 1rem;
& > span {
text-align: center;
}
}
</style> </style>

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="dropdown-question"> <div class="dropdown-question" :class="(errors && errors.length) || hasError ? 'has-error' : ''">
<label :for="inputId" :aria-label="questionText" class="form-label" v-html="labelText"></label> <label :for="inputId" :aria-label="questionText" class="form-label" v-html="labelText"></label>
<select v-model="selectedOption" <select v-model="selectedOption"
class="form-select" class="form-select"
@ -9,8 +9,7 @@
:disabled="isDisabled" :disabled="isDisabled"
:aria-required="isRequired" :aria-required="isRequired"
:validationRules="validationRules" :validationRules="validationRules"
@input="handleChange" >
@blur="handleBlur" >
<option v-for="(value, name, index) in options" :value="name" :key="index"> <option v-for="(value, name, index) in options" :value="name" :key="index">
{{ value }} {{ value }}
</option> </option>
@ -40,10 +39,23 @@ export default {
cmsWidgetName: String, cmsWidgetName: String,
}, },
setup(props) { setup(props) {
const propsClone = Object.assign({}, props);
const modelValue = propsClone.modelValue;
let initialValue;
switch (typeof modelValue) {
case "number":
initialValue = modelValue;
break;
default:
initialValue = (modelValue && modelValue.length > 0) ? modelValue : "";
break;
}
const fieldOptions = { const fieldOptions = {
type: "text", type: "select",
value: props.modelValue, value: props.modelValue,
initialValue: props.modelValue, initialValue: initialValue,
}; };
const { const {

View file

@ -62,6 +62,7 @@ describe("funnel-footer.vue", () => {
const mockMixin = { const mockMixin = {
methods: { methods: {
getCmsContent: jest.fn() getCmsContent: jest.fn(),
getFooterInfoBoxHeight: jest.fn(()=>80)
} }
} }

View file

@ -30,10 +30,10 @@
</div> </div>
<div v-if="!isBackButtonHidden" class="col-auto link-col py-1 text-break"> <div v-if="!isBackButtonHidden" class="col-auto link-col py-1 text-break">
<textLink <textLink
linkType="navigation" linkType="navigation"
:text="backLink" :text="backLink"
@click-event="linkClick" @click-event="linkClick"
href="javascript:void(0)" href="javascript:void(0)"
/> />
</div> </div>
</div> </div>
@ -51,7 +51,6 @@ export default {
isForwardActionDisabled: Boolean, isForwardActionDisabled: Boolean,
isBackButtonHidden: {type: Boolean, default: false}, isBackButtonHidden: {type: Boolean, default: false},
cmsWidgetName: String, cmsWidgetName: String,
}, },
components: { components: {
textLink, textLink,
@ -64,7 +63,7 @@ export default {
} }
}, },
mounted() { mounted() {
this.paddingHeight = document.querySelector(".footer #infoBox").offsetHeight + 24; this.paddingHeight = this.getFooterInfoBoxHeight() + 24;
this.$nextTick(() => { this.$nextTick(() => {
window.addEventListener('resize', this.onResize); window.addEventListener('resize', this.onResize);
}) })
@ -106,7 +105,7 @@ export default {
linkClick() { linkClick() {
this.$emit("BackClicked"); this.$emit("BackClicked");
}, },
} },
}; };
</script> </script>

View file

@ -1,16 +1,9 @@
<template> <template>
<div v-show="isModalVisible" class="loading-modal-backdrop"> <div v-if="isModalVisible" class="loading-modal-backdrop">
<div class="loading-modal"> <div class="loading-modal">
<section class="loading-modal-body"> <section class="loading-modal-body">
<div class="modal-icon-container text-center"> <div class="modal-icon-container text-center">
<img class="loader-gif" alt="Loading" src="@/assets/img/loader.gif"> <object type="image/svg+xml" :data="fmgAnimatedTransition"/>
<img class="modal-icon" alt="" src="@/assets/img/windshield.png">
</div>
<div class="text-center fw-bold fs-5 loading-modal-text">
Please wait...
</div>
<div class="text-center fs-5 loading-modal-text">
This process can take up to 20 seconds.
</div> </div>
</section> </section>
</div> </div>
@ -18,11 +11,15 @@
</template> </template>
<script> <script>
// load the path like this - <object> data attribute doesn't load the relative path correctly
const FMGAnimatedTransition = require("@/assets/img/FMGTransitionAnimation.svg");
export default { export default {
name: 'Modal', name: 'Modal',
data() { data() {
return { return {
isModalVisible: false, isModalVisible: false,
fmgAnimatedTransition: FMGAnimatedTransition,
}; };
}, },
methods: { methods: {
@ -60,23 +57,15 @@
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2); box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2);
border-radius: 4px; border-radius: 4px;
top: 48px; top: 48px;
height: 246px; height: 186px;
width: 327px; width: 327px;
} }
.loading-modal-body {
position: relative;
padding: 20px 10px;
}
.loading-modal-text { .loading-modal-text {
padding: 0 24px 0 24px; padding: 0 24px 0 24px;
} }
.modal-icon-container { .modal-icon-container {
position: relative;
width: 75px;
height: 75px;
margin: 15px auto; margin: 15px auto;
padding-bottom: 26px; padding-bottom: 26px;
} }
@ -85,6 +74,8 @@
position: absolute; position: absolute;
vertical-align: middle; vertical-align: middle;
border: 0; border: 0;
width: 327px;
height: 186px;
left: 50%; left: 50%;
top: 50%; top: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);

View file

@ -2,8 +2,8 @@
<div class="textbox-question" :class="(errors && errors.length) || hasError ? 'has-error' : ''"> <div class="textbox-question" :class="(errors && errors.length) || hasError ? 'has-error' : ''">
<label :for="inputId" :aria-label="questionText" class="form-label" v-html="labelText"></label> <label :for="inputId" :aria-label="questionText" class="form-label" v-html="labelText"></label>
<!-- See https://stackoverflow.com/a/30976223 for information about "do-not-autofill" --> <!-- See https://stackoverflow.com/a/30976223 for information about "do-not-autofill" -->
<input <input
v-model="value" v-model.trim="value"
v-maska="mask" v-maska="mask"
:type="type" :type="type"
class="form-control" class="form-control"
@ -14,45 +14,47 @@
:aria-disabled="isDisabled" :aria-disabled="isDisabled"
:disabled="isDisabled" :disabled="isDisabled"
:aria-required="isRequired" :aria-required="isRequired"
autocomplete="do-not-autofill" autocomplete="do-not-autofill"
:class="[hasIcon ? 'has-icon' : '', iconRight ? 'icon-right' : '']" :class="[hasIcon ? 'has-icon' : '', iconRight ? 'icon-right' : '']"
:validationRules="validationRules" :validationRules="validationRules"
@change="handleChange"
@blur="handleChange"
/> />
<div v-show="errorMessage" class="row mt-2 form-test-error"> <div v-show="errorMessage" class="row my-2 form-test-error">
<span role="alert">{{ errorMessage }}</span> <span class="d-inline-flex mt-0" role="alert">{{ errorMessage }}</span>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { useField, validate } from "vee-validate";
import { useField } from "vee-validate";
export default { export default {
name: "textbox-question", name: "textbox-question",
props: { props: {
type: { type: {
type: String, type: String,
default: 'text', default: "text",
}, },
placeholderText: { placeholderText: {
type: String, type: String,
default: '', default: "",
}, },
modelValue: String, modelValue: String,
inputId: String, inputId: String,
isDisabled: Boolean, isDisabled: Boolean,
isRequired: Boolean, isRequired: Boolean,
disableAutoFill: Boolean, disableAutoFill: Boolean,
hasIcon: Boolean, // If input has an icon hasIcon: Boolean, // If input has an icon
iconRight: Boolean, // Place icon on right side of text input, otherwise default is left if hasIcon prop is used iconRight: Boolean, // Place icon on right side of text input, otherwise default is left if hasIcon prop is used
hasError: Boolean, hasError: Boolean,
mask: { mask: {
type: String, type: String,
default: '', default: "",
}, },
validationRules: String, validationRules: String,
cmsWidgetName: String semiAggressiveValidation: Boolean,
cmsWidgetName: String,
}, },
setup(props) { setup(props) {
const propsClone = Object.assign({}, props); const propsClone = Object.assign({}, props);
@ -60,7 +62,7 @@ export default {
let initialValue; let initialValue;
switch (typeof modelValue) { switch (typeof modelValue) {
case "number": case "number":
initialValue = modelValue; initialValue = modelValue;
break; break;
default: default:
@ -71,17 +73,11 @@ export default {
const fieldOptions = { const fieldOptions = {
type: "text", type: "text",
value: modelValue, value: modelValue,
initialValue: initialValue initialValue: initialValue,
}; };
const { const { errorMessage, handleBlur, handleChange, meta, validate, errors } =
errorMessage, useField(props.inputId, props.validationRules, fieldOptions);
handleBlur,
handleChange,
meta,
validate,
errors,
} = useField(props.inputId, props.validationRules, fieldOptions);
return { return {
errorMessage, errorMessage,
@ -93,16 +89,16 @@ export default {
}; };
}, },
computed: { computed: {
questionText(){ questionText() {
return this.getCmsContent(this.cmsWidgetName, 'QuestionText'); return this.getCmsContent(this.cmsWidgetName, "QuestionText");
}, },
value: { value: {
get: function() { get: function () {
return this.modelValue; return this.modelValue;
}, },
set: function(newValue) { set: function (newValue) {
this.$emit("update:modelValue", newValue); this.$emit("update:modelValue", newValue);
} },
}, },
labelText: { labelText: {
get: function () { get: function () {
@ -112,7 +108,11 @@ export default {
var words = this.questionText.toString().split(/[ ]+/); var words = this.questionText.toString().split(/[ ]+/);
words.forEach(function (word) { words.forEach(function (word) {
const position = 1; const position = 1;
word = [word.toString().slice(0, position), noBreakChar, word.toString().slice(position)].join(''); word = [
word.toString().slice(0, position),
noBreakChar,
word.toString().slice(position),
].join("");
questionText += `${word} `; questionText += `${word} `;
}); });
@ -122,14 +122,19 @@ export default {
} }
return questionText; return questionText;
} },
} },
}, },
watch: { watch: {
value(newValue) { async value(newValue) {
this.handleChange(newValue); if (this.semiAggressiveValidation) {
} 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
}
}
},
},
}; };
</script> </script>
@ -159,6 +164,7 @@ export default {
border: 1px solid $gray-500; border: 1px solid $gray-500;
border-radius: .5rem; border-radius: .5rem;
min-height: 3rem; min-height: 3rem;
padding: 12px 16px;
&::placeholder { &::placeholder {
color: $gray-500; color: $gray-500;
} }
@ -182,4 +188,4 @@ export default {
display: none; display: none;
} }
} }
</style> </style>

View file

@ -18,11 +18,12 @@ const errorMessages = {
LAST_NAME_REQUIRED: "Please enter your last name", LAST_NAME_REQUIRED: "Please enter your last name",
EMAIL_ADDRESS_REQUIRED: "Please enter your email address", EMAIL_ADDRESS_REQUIRED: "Please enter your email address",
EMAIL_ADDRESS_FORMAT: "Please enter a valid email address", EMAIL_ADDRESS_FORMAT: "Please enter a valid email address",
SERVICE_ZIP_REQUIRED: "Please enter your Service ZIP", SERVICE_ZIP_REQUIRED: "Please enter your service ZIP",
SERVICE_ZIP_FORMAT: "Please enter a valid Service ZIP", SERVICE_ZIP_FORMAT: "Please enter a valid service ZIP",
VIN_REQUIRED: "Please enter your VIN", VIN_REQUIRED: "Please enter your VIN",
VIN_FORMAT: "Invalid VIN. Please make sure that you entered the correct 17-digit, alpha-numeric number. VINs do not contain the letters I, O, or Q", VIN_FORMAT: "Invalid VIN. Please make sure that you entered the correct 17-digit, alpha-numeric number. VINs do not contain the letters I, O, or Q",
OPTION_REQUIRED: "Please select an option", OPTION_REQUIRED: "Please select an option",
VEHICLE_REQUIRED: "Please select a vehicle",
}; };
export { errorMessages }; export { errorMessages };

View file

@ -22,6 +22,7 @@ const storeActions = {
LOG_PAGE_VIEW: "logPageView", LOG_PAGE_VIEW: "logPageView",
LOG_CUSTOM_EVENT: "logCustomEvent", LOG_CUSTOM_EVENT: "logCustomEvent",
GET_EXPERIMENTS_BY_USER: "GetExperimentsByUser", GET_EXPERIMENTS_BY_USER: "GetExperimentsByUser",
UPDATE_SERVICE_LOCATION_WITH_VEHICLE_REGISTRATION: "updateServiceLocationWithVehicleRegistration",
// DEPENDENCY MUTATIONS // DEPENDENCY MUTATIONS
RESET_VEHICLE_STATE_AND_DEPENDENCIES: "resetVehicleAndDependencies", RESET_VEHICLE_STATE_AND_DEPENDENCIES: "resetVehicleAndDependencies",

View file

@ -44,8 +44,8 @@ const storeMutations = {
// OTHER MUTATIONS // OTHER MUTATIONS
UPDATE_PAGE_DATA: "updatePageData", UPDATE_PAGE_DATA: "updatePageData",
UPDATE_STATE_WITH_ORDER_INFORMATION: "updateStateWithOrderInformation" UPDATE_STATE_WITH_ORDER_INFORMATION: "updateStateWithOrderInformation",
UPDATE_SERVICE_LOCATION_WITH_VEHICLE_REGISTRATION: "updateServiceLocationWithVehicleRegistration"
}; };
export { storeMutations }; export { storeMutations };

View file

@ -4,8 +4,18 @@ import { storeActions } from "@/constants/store-actions";
export function getDamageString() { export function getDamageString() {
const damageLocations = store.getters.damage.glassToReplace; const damageLocations = store.getters.damage.glassToReplace;
const isRepair = store.getters.damage.isRepair;
let returnString; let returnString;
if(damageLocations.length > 1){ if (!damageLocations) {
return;
}
// If it's a repair it's always a windshield.
if(isRepair){
return "windshield"
}
if (damageLocations.length > 1) {
returnString = "match" returnString = "match"
} else { } else {
switch(damageLocations[0]?.location) { switch(damageLocations[0]?.location) {

View file

@ -3,8 +3,10 @@ import { externalUrls } from "@/router/router-constants/externalUrl-values";
import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js"; import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js";
import { saveOrder } from "@/helpers/heritage-integration/order-helper.js"; import { saveOrder } from "@/helpers/heritage-integration/order-helper.js";
import { fmgPageValues } from "@/router/router-constants/fmgPage-values"; import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import { storeActions } from "@/constants/store-actions";
import store from "@/store"; import store from "@/store";
import router from "@/router"; import router from "@/router";
import baseMixin from "@/mixins/base-mixin.js";
/* /*
If the user has visited the funnel before this method will determine the bets place to If the user has visited the funnel before this method will determine the bets place to
@ -12,7 +14,6 @@ import router from "@/router";
the user has an existing order and they come back in from the Safelite.com CTA. the user has an existing order and they come back in from the Safelite.com CTA.
*/ */
export async function getPageToRouteExistingOrderTo(toRoute = {}, existingHeritageOrder = false) { export async function getPageToRouteExistingOrderTo(toRoute = {}, existingHeritageOrder = false) {
// If the user is coming in via the Safelite.Com CTA // If the user is coming in via the Safelite.Com CTA
if (toRoute.query[queryStrings.START_TYPE] === 'fmg') { if (toRoute.query[queryStrings.START_TYPE] === 'fmg') {
// If they have an existing order, return 'heritage' for the page name. // If they have an existing order, return 'heritage' for the page name.
@ -56,6 +57,7 @@ export async function navigateToHeritageFunnel() {
export async function navigateAfterSaveToHeritageFunnel(currentRoute) { export async function navigateAfterSaveToHeritageFunnel(currentRoute) {
const currentComponent = currentRoute.matched[0].components; const currentComponent = currentRoute.matched[0].components;
currentComponent.default.methods.resetDependentState(); currentComponent.default.methods.resetDependentState();
// Create the order (or save existing order) when navigating to Heritage Funnel. // Create the order (or save existing order) when navigating to Heritage Funnel.
await saveOrder(); await saveOrder();

View file

@ -12,28 +12,28 @@
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" ref="funnelSubHeader" /> <funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" ref="funnelSubHeader" />
<div class="fade-on-route-transition sub-container make-tall"> <div class="fade-on-route-transition sub-container make-tall">
<customerQuestions ref="customerQuestions" v-model="customerQuestions" /> <customerQuestions ref="customerQuestions" v-model="customerQuestions" />
<alert ref="alertVinNotFound" v-show="displayVinNotFoundAlert" <alert ref="alertVinNotFound" v-if="displayVinNotFoundAlert"
class="my-3" class="mb-4"
cmsWidgetName="AlertVinNotFoundWidget" cmsWidgetName="AlertVinNotFoundWidget"
alertClass="alert-danger" alertClass="alert-danger"
v-bind:isDismissible="false" v-bind:isDismissible="false"
/> />
<alert ref="alertMatchedDifferentVehicle" v-show="displayMatchedDifferentVehicleAlert" <alert ref="alertMatchedDifferentVehicle" v-if="displayMatchedDifferentVehicleAlert"
class="my-3" class="mb-4"
:manualHeadline="AlertMatchedDifferentVehicleHeader" :manualHeadline="AlertMatchedDifferentVehicleHeader"
:manualCopy="AlertMatchedDifferentVehicleBody" :manualCopy="AlertMatchedDifferentVehicleBody"
alertClass="alert-warning" alertClass="alert-warning"
v-bind:isDismissible="false" v-bind:isDismissible="false"
/> />
<alert ref="alertNonServiceableZip" v-show="displayNonServiceableZipAlert" <alert ref="alertNonServiceableZip" v-if="displayNonServiceableZipAlert"
class="my-3" class="mb-4"
alertClass="alert-danger" alertClass="alert-danger"
:manualHeadline="AlertNonServiceableZipHeader" :manualHeadline="AlertNonServiceableZipHeader"
:manualCopy="AlertNonServiceableZipBody" :manualCopy="AlertNonServiceableZipBody"
v-bind:isDismissible="false" v-bind:isDismissible="false"
/> />
<alert ref="alertVinLookupsByHomeAddressNotAllowed" v-show="displayVinLookupByHomeAddressNotAllowedAlert" <alert ref="alertVinLookupsByHomeAddressNotAllowed" v-if="displayVinLookupByHomeAddressNotAllowedAlert"
class="my-3" class="mb-4"
cmsWidgetName="AlertVinLookupsByHomeAddressNotAllowedWidget" cmsWidgetName="AlertVinLookupsByHomeAddressNotAllowedWidget"
alertClass="alert-danger" alertClass="alert-danger"
v-bind:isDismissible="false" v-bind:isDismissible="false"
@ -72,10 +72,8 @@ import alert from "@/ux-components/alert/alert";
import textboxQuestion from "@/common-components/textbox-question/textbox-question"; import textboxQuestion from "@/common-components/textbox-question/textbox-question";
import loadingModal from '@/common-components/loading-modal/loading-modal.vue'; import loadingModal from '@/common-components/loading-modal/loading-modal.vue';
import { Form } from "vee-validate"; import { Form, defineRule } from "vee-validate";
import { defineRule } from "vee-validate"; import { required, regex } from "@/helpers/validation-rules";
import { required } from "@/helpers/validation-rules";
import { regex } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages"; import { errorMessages } from "@/constants/error-messages";
// Supporting files // Supporting files
@ -262,6 +260,15 @@ export default {
return; return;
} }
// if multiple cars were found
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 // update data if the zip or service zip is servicable
this.updateCustomerInfo(); this.updateCustomerInfo();
} }
@ -275,6 +282,8 @@ export default {
this.displayVinLookupByHomeAddressNotAllowedAlert = false; this.displayVinLookupByHomeAddressNotAllowedAlert = false;
}, },
navigateForward(carEntered, carsFound) { navigateForward(carEntered, carsFound) {
this.updateServiceLocationIfNecessary();
if (carsFound.length == 1) { if (carsFound.length == 1) {
// if a different vehicle is found than the one entered and the selected glass // if a different vehicle is found than the one entered and the selected glass
// is not available for that vehicle // is not available for that vehicle
@ -293,16 +302,18 @@ export default {
navigateAfterSaveToHeritageFunnel(this.$route); navigateAfterSaveToHeritageFunnel(this.$route);
} }
} else if (carsFound.length > 1) { } else if (carsFound.length > 1) {
// if multiple cars were found // if multiple cars were found
if (carsFound.find(car => car.vehicle.carId === carEntered.carId)) { let matchingCars = carsFound.filter(car => car.vehicle.carId === carEntered.carId);
// and one of them matches the car id entered if (matchingCars.length === 1) {
this.$refs.loadingModal.showModal(); this.$refs.loadingModal.showModal();
// and one and only of them matches the car id entered
const matchingCar = matchingCars[0];
this.updateVehicleInfo(matchingCar.vin, matchingCar.vehicle);
navigateAfterSaveToHeritageFunnel(this.$route); navigateAfterSaveToHeritageFunnel(this.$route);
} else { } else {
// and there is no match, navigate to "address-vehicles" page // if there are no matches or there are multiple matches, navigate to "address-vehicles" page
this.$router.navigateAfterSave(this.navigationScenarios.CONTINUING_WITH_MULTIPLE_VEHICLES, this.$router.navigateAfterSave(this.navigationScenarios.CONTINUING_WITH_MULTIPLE_VEHICLES, this.$route, {}, {}, carsFound);
this.$route, {}, {},
carsFound);
} }
} }
@ -344,8 +355,16 @@ export default {
store.commit(storeMutations.UPDATE_REGISTRATION_LAST_NAME, this.customerQuestions.lastName); store.commit(storeMutations.UPDATE_REGISTRATION_LAST_NAME, this.customerQuestions.lastName);
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP_CODE, this.serviceZipCode); store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP_CODE, this.serviceZipCode);
store.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, this.customerQuestions.emailAddress); store.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, this.customerQuestions.emailAddress);
}, },
updateServiceLocationIfNecessary() {
const serviceLocation = store.getters.order.serviceLocation;
if (!serviceLocation.address && serviceLocation.zipCode && serviceLocation.zipCode == store.getters.vehicle.registration.zipCode) {
baseMixin.methods.dispatchStoreAction(
storeActions.UPDATE_SERVICE_LOCATION_WITH_VEHICLE_REGISTRATION
);
}
}
}, },
mounted() { mounted() {
this.attachCustomEvents(); this.attachCustomEvents();
@ -364,13 +383,14 @@ export default {
return text; return text;
}, },
AlertMatchedDifferentVehicleBody(){ AlertMatchedDifferentVehicleBody(){
let content = this.getCmsContent("AlertMatchedDifferentVehicleWidget", "BodyText");
content = content.replaceAll("{custom:glassText}", getDamageString());
const vinYmmFound = `${this.customAlertData?.vehicleInfo?.year} ${this.customAlertData?.vehicleInfo?.make} ${this.customAlertData?.vehicleInfo?.model}`;
const vinYmmExpected = `${store.getters.vehicle.year} ${store.getters.vehicle.make} ${store.getters.vehicle.model}`;
content = content.replaceAll("{custom:vinYmmFound}", vinYmmFound); const vinYmmFound = `${this.customAlertData?.vehicleInfo?.year} ${this.customAlertData?.vehicleInfo?.make} ${this.customAlertData?.vehicleInfo?.model}`;
content = content.replaceAll("{custom:vinYmmExpected}", vinYmmExpected); const vinYmmExpected = `${store.getters.vehicle.year} ${store.getters.vehicle.make} ${store.getters.vehicle.model}`;
const content = this.getCmsContent("AlertMatchedDifferentVehicleWidget", "BodyText")
.replaceAll("{custom:glassText}", getDamageString())
.replaceAll("{custom:vinYmmFound}", vinYmmFound)
.replaceAll("{custom:vinYmmExpected}", vinYmmExpected);
return content; return content;
}, },

View file

@ -1,32 +1,70 @@
<template> <template>
<div class="row mt-2 mb-4"> <div class="row mt-2 mb-4">
<div class="col"> <div class="col">
<textboxQuestion cmsWidgetName="StreetAddressQuestionWidget" v-model="addressModel.streetAddress" ref="autocomplete" inputId="autocomplete" placeholderText="Search" aria-haspopup="" hasIcon disableAutoFill validationRules="street-address-required" /> <textboxQuestion
id="streetAddressField"
cmsWidgetName="StreetAddressQuestionWidget"
v-model="addressModel.streetAddress"
ref="autocomplete"
inputId="autocomplete"
placeholderText="Search"
aria-haspopup=""
hasIcon
disableAutoFill
validationRules="street-address-required"
/>
</div> </div>
</div> </div>
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
<div class="row mb-4" v-show="showAddressFields" aria-live="polite"> <div class="row mb-4" v-show="showAddressFields" aria-live="polite">
<div class="col"> <div class="col">
<textboxQuestion cmsWidgetName="CityQuestionWidget" v-model="addressModel.city" ref="city" inputId="cbf28188fdf2436688fd735915f7ee56" disableAutoFill validationRules="city-required"/> <textboxQuestion
cmsWidgetName="CityQuestionWidget"
v-model="addressModel.city"
ref="city"
inputId="cbf28188fdf2436688fd735915f7ee56"
disableAutoFill
validationRules="city-required"
semiAggressiveValidation
/>
</div> </div>
</div> </div>
</transition> </transition>
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
<div class="row mb-4" v-show="showAddressFields" aria-live="polite"> <div class="row mb-4" v-show="showAddressFields" aria-live="polite">
<div class="col"> <div class="col">
<dropdownQuestion cmsWidgetName="StateQuestionWidget" v-model="addressModel.state" ref="state" inputId="8fdf9dc2e13e430eb57529499dceb3eb" :options="stateOptions" disableAutoFill validationRules="state-required" /> <dropdownQuestion
cmsWidgetName="StateQuestionWidget"
v-model="addressModel.state"
ref="state"
inputId="8fdf9dc2e13e430eb57529499dceb3eb"
:options="stateOptions"
disableAutoFill
validationRules="state-required"
/>
</div> </div>
<div class="col"> <div class="col">
<textboxQuestion cmsWidgetName="ZipQuestionWidget" v-model="addressModel.zipCode" ref="zipCode" inputId="01a9a1c2de0b4c9da8e023c9ae3be498" mask="#####" disableAutoFill validationRules="zip-code-required|zip-code-format"/> <textboxQuestion
cmsWidgetName="ZipQuestionWidget"
v-model="addressModel.zipCode"
ref="zipCode"
inputId="01a9a1c2de0b4c9da8e023c9ae3be498"
mask="#####"
disableAutoFill
validationRules="zip-code-required|zip-code-format"
semiAggressiveValidation
/>
</div> </div>
</div> </div>
</transition> </transition>
<alert ref="alertVerificationWarning" v-show="displayVerificationWarning" <alert ref="alertVerificationWarning" v-if="displayVerificationWarning"
class="mb-4"
cmsWidgetName="AlertVerificationWarningWidget" cmsWidgetName="AlertVerificationWarningWidget"
alertClass="alert-warning" alertClass="alert-warning"
v-bind:isDismissible="false" v-bind:isDismissible="false"
/> />
<alert ref="alertNoMatchWarning" v-show="displayNoMatchWarning" <alert ref="alertNoMatchWarning" v-if="displayNoMatchWarning"
class="mb-4"
cmsWidgetName="AlertNoMatchWarningWidget" cmsWidgetName="AlertNoMatchWarningWidget"
alertClass="alert-warning" alertClass="alert-warning"
v-bind:isDismissible="false" v-bind:isDismissible="false"
@ -34,18 +72,19 @@
</template> </template>
<script> <script>
import textboxQuestion from "@/common-components/textbox-question/textbox-question"; import textboxQuestion from "@/common-components/textbox-question/textbox-question";
import dropdownQuestion from "@/common-components/dropdown-question/dropdown-question"; import dropdownQuestion from "@/common-components/dropdown-question/dropdown-question";
import alert from "@/ux-components/alert/alert"; import alert from "@/ux-components/alert/alert";
import { applicationConfig } from "@/constants/application-config.js"; import { applicationConfig } from "@/constants/application-config.js";
import { defineRule } from "vee-validate"; import { defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules"; import { required, regex } from "@/helpers/validation-rules";
import { regex } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages"; import { errorMessages } from "@/constants/error-messages";
// DEFINE VALIDATION RULES // DEFINE VALIDATION RULES
defineRule("street-address-required", required(errorMessages.STREET_ADDRESS_REQUIRED)); defineRule(
"street-address-required",
required(errorMessages.STREET_ADDRESS_REQUIRED)
);
defineRule("city-required", required(errorMessages.CITY_REQUIRED)); defineRule("city-required", required(errorMessages.CITY_REQUIRED));
defineRule("state-required", required(errorMessages.STATE_REQUIRED)); defineRule("state-required", required(errorMessages.STATE_REQUIRED));
defineRule("zip-code-required", required(errorMessages.ZIP_REQUIRED)); defineRule("zip-code-required", required(errorMessages.ZIP_REQUIRED));
@ -152,7 +191,7 @@ export default ({
this.addressModel.state !== null & this.addressModel.state !== null &
this.addressModel.zipCode !== null) { this.addressModel.zipCode !== null) {
this.showAddressFields = true; this.showAddressFields = true;
} }
const addressField1 = document.getElementById("autocomplete"); const addressField1 = document.getElementById("autocomplete");
@ -173,44 +212,52 @@ export default ({
); );
// Standard place_changed event handling // Standard place_changed event handling
autocomplete.addListener('place_changed', fillInAddress); const autocompleteListener = window.google.maps.event.addListener(autocomplete, 'place_changed', fillInAddress);
// Wrapping the addressField1 element in the Google Address Autocomplete object // Wrapping the addressField1 element in the Google Address Autocomplete object
// will cause "autocomplete='off'" which Chrome completely ignores. This event // will cause "autocomplete='off'" which Chrome completely ignores. This event
// handler will set the value to something arbitrary so autofill doesn't work. // handler will set the value to something arbitrary so autofill doesn't work.
// https://stackoverflow.com/a/30976223 // https://stackoverflow.com/a/30976223
addressField1.addEventListener("focus", () => { addressField1.addEventListener("focus", () => {
addressField1.setAttribute("autocomplete", "do-not-autofill"); addressField1.setAttribute("autocomplete", "do-not-autofill");
// Make place results box stick to the input on scroll
const streetAddressField = document.getElementById("streetAddressField");
const autocompleteResultsContainer = document.getElementsByClassName("pac-container")[0];
if (autocompleteResultsContainer) {
streetAddressField.appendChild(autocompleteResultsContainer);
}
}) })
addressField1.onchange = function() { addressField1.onchange = function() {
const hover = document.querySelector(".pac-container .pac-item:hover"); const hover = document.querySelector(".pac-container .pac-item:hover");
// if an item has been clicked, do nothing, otherwise get first solution and use Geocoder to get the place // if an item has been clicked, do nothing, otherwise get first solution and use Geocoder to get the place
if (hover === null) { if (hover === null) {
const item = document.querySelector(".pac-container .pac-item"); const item = document.querySelector(".pac-container .pac-item");
if (item != null) { if (item != null) {
const firstResult = item.textContent; const firstResult = item.textContent;
const geocoder = new window.google.maps.Geocoder(); const geocoder = new window.google.maps.Geocoder();
geocoder.geocode({ geocoder.geocode({
address: firstResult address: firstResult
}, function (results, status) { }, function (results, status) {
if (status === window.google.maps.GeocoderStatus.OK) { if (status === window.google.maps.GeocoderStatus.OK) {
fillInAddress(results[0]); fillInAddress(results[0]);
self.displayVerificationWarning = true; self.displayVerificationWarning = true;
self.displayNoMatchWarning = false; self.displayNoMatchWarning = false;
} }
}); });
}
else {
self.addressModel.city = "";
self.addressModel.state = "";
self.addressModel.zipCode = "";
self.showAddressFields = true;
self.displayVerificationWarning = false;
self.displayNoMatchWarning = true;
}
} }
else { };
self.addressModel.city = "";
self.addressModel.state = "";
self.addressModel.zipCode = "";
self.showAddressFields = true;
self.displayVerificationWarning = false;
self.displayNoMatchWarning = true;
}
}
};
function fillInAddress(place) { function fillInAddress(place) {
if (!place) { if (!place) {
@ -218,19 +265,19 @@ export default ({
} }
if (place && place.address_components) { if (place && place.address_components) {
self.addressModel.streetAddress= "";
self.showAddressFields = true; self.showAddressFields = true;
for (const component of place.address_components) { for (const component of place.address_components) {
const componentType = component.types[0]; const componentType = component.types[0];
switch (componentType) { switch (componentType) {
case "street_number": { case "street_number": {
self.addressModel.streetAddress = component.long_name; self.addressModel.streetAddress = component.long_name;
break; break;
} }
case "route": { case "route": {
self.addressModel.streetAddress += ' ' + component.short_name; self.addressModel.streetAddress +=
" " + component.short_name;
break; break;
} }
case "locality": { case "locality": {
@ -239,15 +286,15 @@ export default ({
} }
case "administrative_area_level_1": { case "administrative_area_level_1": {
self.addressModel.state = component.short_name; self.addressModel.state = component.short_name;
// self.addressModel.state = "";
break; break;
} }
case "postal_code": { case "postal_code": {
self.addressModel.zipCode = component.long_name; self.addressModel.zipCode = component.long_name;
break; break;
} }
} }
} }
self.displayVerificationWarning = false; self.displayVerificationWarning = false;
self.displayNoMatchWarning = false; self.displayNoMatchWarning = false;
@ -255,13 +302,21 @@ export default ({
else { else {
self.displayVerificationWarning = true; self.displayVerificationWarning = true;
self.displayNoMatchWarning = false; self.displayNoMatchWarning = false;
} }
}
}) // after showing the address fields, disable the address autocomplete
.catch(() => { window.google.maps.event.removeListener(autocompleteListener);
// Failed to fetch script window.google.maps.event.clearInstanceListeners(autocomplete);
console.log("Unable to load Google Places API script"); addressField1.onchange = null;
}); const pacContainer = document.querySelector(".pac-container");
pacContainer.remove();
}
})
.catch(() => {
// Failed to fetch script
console.log("Unable to load Google Places API script");
});
} }
}, },
mounted() { mounted() {
@ -269,8 +324,17 @@ export default ({
}, },
watch: { watch: {
addressModel: { addressModel: {
handler(newValue){ handler(newValue) {
this.displayNoMatchWarning = false; // The first time the address model changes is w
if (!newValue.city &&
!newValue.state &&
!newValue.zipCode) {
return;
}
if (this.displayNoMatchWarning === true) {
this.displayNoMatchWarning = false;
}
}, },
deep: true deep: true
} }
@ -281,4 +345,15 @@ export default ({
alert, alert,
} }
}) })
</script> </script>
<style lang="scss">
#streetAddressField {
position: relative;
.pac-container {
top: 76px !important; // Height of #streetAddressField
left: 0 !important;
}
}
</style>

View file

@ -2,17 +2,39 @@
<addressQuestions ref="addressQuestions" v-model="customerModel.addressQuestions" :alertNotifications="alertNotifications" /> <addressQuestions ref="addressQuestions" v-model="customerModel.addressQuestions" :alertNotifications="alertNotifications" />
<div class="row mb-4"> <div class="row mb-4">
<div class="col"> <div class="col">
<textboxQuestion cmsWidgetName="FirstNameQuestionWidget" v-model="customerModel.firstName" ref="firstName" inputId="08497a2efd9a4a73a70360ab47b4838d" disableAutoFill validationRules="first-name-required" /> <textboxQuestion
cmsWidgetName="FirstNameQuestionWidget"
v-model="customerModel.firstName"
ref="firstName"
inputId="08497a2efd9a4a73a70360ab47b4838d"
disableAutoFill
validationRules="first-name-required"
/>
</div> </div>
</div> </div>
<div class="row mb-4"> <div class="row mb-4">
<div class="col"> <div class="col">
<textboxQuestion cmsWidgetName="LastNameQuestionWidget" v-model="customerModel.lastName" ref="lastName" inputId="0030e56a57e74a4ab92de7fb8e97fec5" disableAutoFill validationRules="last-name-required" /> <textboxQuestion
cmsWidgetName="LastNameQuestionWidget"
v-model="customerModel.lastName"
ref="lastName"
inputId="0030e56a57e74a4ab92de7fb8e97fec5"
disableAutoFill
validationRules="last-name-required"
/>
</div> </div>
</div> </div>
<div class="row mb-4"> <div class="row mb-5">
<div class="col"> <div class="col">
<textboxQuestion cmsWidgetName="EmailAddressQuestionWidget" v-model="customerModel.emailAddress" ref="emailAddress" inputId="00450a91b8964a768ce3992e6feb890f" disableAutoFill validationRules="email-address-required|email-address-format"/> <textboxQuestion
cmsWidgetName="EmailAddressQuestionWidget"
v-model="customerModel.emailAddress"
ref="emailAddress"
inputId="00450a91b8964a768ce3992e6feb890f"
disableAutoFill
validationRules="email-address-required|email-address-format"
semiAggressiveValidation
/>
</div> </div>
</div> </div>
</template> </template>
@ -29,7 +51,7 @@ import { errorMessages } from "@/constants/error-messages";
defineRule("first-name-required", required(errorMessages.FIRST_NAME_REQUIRED)); defineRule("first-name-required", required(errorMessages.FIRST_NAME_REQUIRED));
defineRule("last-name-required", required(errorMessages.LAST_NAME_REQUIRED)); defineRule("last-name-required", required(errorMessages.LAST_NAME_REQUIRED));
defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED)); defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
defineRule("email-address-format", regex(/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9_\-.]+).([a-zA-Z]{2,})$/, errorMessages.EMAIL_ADDRESS_FORMAT)); defineRule("email-address-format", regex(/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9_\-.]+)\.([a-zA-Z]{2,})$/, errorMessages.EMAIL_ADDRESS_FORMAT));
export default ({ export default ({
name: "customer-questions", name: "customer-questions",

View file

@ -0,0 +1,113 @@
import { shallowMount } from "@vue/test-utils";
import addressVehiclesQuestion from "@/layouts/address-vehicles/address-vehicles-question/address-vehicles-question";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
// import store from "@/store";
// jest.mock("@/store", () => { return {}; }, { virtual: true });
describe("addressVehiclesQuestion.vue", () => {
it("Should include button-question component", () => {
// Arrange
const wrapper = shallowMount(addressVehiclesQuestion, {
propsData: {
// vehicles: [
// {
// "Name": "testname",
// "Text": "testtext",
// }
// ]
}
});
// console.log('wrapper.html: ', wrapper.html());
// Assert
const buttonQuestion = wrapper.find('button-question-stub');
expect(buttonQuestion).toBe;
});
it("on initialize should pass in questionText", () => {
// Arrange
const wrapper = shallowMount(addressVehiclesQuestion, {
propsData: {},
});
//Act
addressVehiclesQuestion.methods.initializeComponent.call(wrapper.vm, {QuestionText: 'Testing question text'});
// console.log('wrapper.html: ', wrapper.html());
// console.log('wrapper.vm.questionText: ', wrapper.vm.questionText);
// Assert
expect(wrapper.vm.questionText).toEqual('Testing question text');
});
// it("Alert should show if prop isCarIdDifferent is true", () => {
// // Arrange
// const wrapper = shallowMount(addressVehiclesQuestion, setupMountOptions({
// propsData: {
// isCarIdDifferent: true,
// }
// }));
// //Act
// const alert = wrapper.find('alert');
// console.log('wrapper.html: ', wrapper.html());
// console.log('wrapper.vm.questionText: ', wrapper.vm.questionText);
// // Assert
// // expect(wrapper.vm.questionText).toEqual('Testing question text');
// });
});
function setupMountOptions(mountOptionsMockData = {}) {
// //Mock store
// store.dispatch = jest.fn(() => {});
// store.getters = {};
// const mockMixin = {
// methods: {
// getCmsContent: jest.fn().mockImplementation(() => {
// return '';
// }),
// getDamageString: jest.fn().mockImplementation(() => {
// return '';
// })
// },
// store: {
// dispatch: store.dispatch,
// getters: store.getters,
// },
// }
const mockGetCmsContent = jest.fn();
mockGetCmsContent((cmsWidget, field) => {
return field
});
const defaultMountOptions = {
// route: { query: { fmgPage: 'page-name' } },
mixins: {
methods: {
getCmsContent: mockGetCmsContent,
},
},
global: {
mocks: {
store: {
dispatch: store.dispatch,
getters: store.getters,
},
},
},
};
const baseMountOptions = getMountOptions(Object.assign(defaultMountOptions, mountOptionsMockData));
const allMountOptions = Object.assign(defaultMountOptions, baseMountOptions);
console.log('what are allMountOptions??? ', allMountOptions);
return allMountOptions;
}

View file

@ -0,0 +1,94 @@
<template>
<buttonQuestion
class="address-vehicles-question"
buttonType="listButton"
groupName="ChooseAddressVehicle"
:questionText="questionText"
:answers="vehicles"
v-model="selectedVehicleVin"
isRequired=true
:validation-rules="validationRules"
/>
<alert
ref="differentVehicleAlert"
v-if="isCarIdDifferent"
class="my-3"
alertClass="alert-warning"
:manualHeadline="differentVehicleAlertHeader"
:manualCopy="differentVehicleAlertBody"
v-bind:isDismissible="false"
/>
</template>
<script>
import buttonQuestion from "@/common-components/button-question/button-question";
import alert from "@/ux-components/alert/alert";
// Supporting files
import { getDamageString } from "@/helpers/damage-helper";
export default {
name: "address-vehicles-question",
props: {
vehicles: Array,
modelValue: String,
cmsWidgetName: String,
validationRules: String,
isCarIdDifferent: Boolean,
},
computed: {
differentVehicleAlertHeader() {
return this.getCmsContent("FoundWindshield", "HeadlineText").replaceAll("{custom:damage}", getDamageString());
},
differentVehicleAlertBody() {
return this.getCmsContent(
"FoundWindshield",
"BodyText"
)
.replaceAll("{custom:damage}", getDamageString())
.replaceAll(
"{custom:vinlookupYear}",
this.selectedVehicle?.vehicle.year
)
.replaceAll(
"{custom:vinlookupMake}",
this.selectedVehicle?.vehicle.make
)
.replaceAll(
"{custom:vinlookupModel}",
this.selectedVehicle?.vehicle.model
);
},
questionText() {
return this.getCmsContent("VehicleConfirmationQuestion", "QuestionText");
},
selectedVehicleVin: {
get: function() {
return this.modelValue;
},
set: function(newValue) {
this.$emit("update:modelValue", newValue);
}
},
selectedVehicle() {
return this.vehicles.find( ({ vin }) => vin === this.selectedVehicleVin[0] );
},
},
components: {
buttonQuestion,
alert,
},
};
</script>
<style lang="scss">
.address-vehicles-question {
.question-text {
margin-bottom: .5rem;
& > span {
text-align: left;
}
}
}
</style>

View file

@ -0,0 +1,26 @@
import { shallowMount } from "@vue/test-utils";
import addressVehicles from "@/layouts/address-vehicles/address-vehicles";
describe("addressVehicles.vue", () => {
it("Should include address-vehicles-question component", () => {
// Arrange
const wrapper = shallowMount(addressVehicles, {
propsData: {
vehicles: [
{
"Name": "testname",
"Text": "testtext",
}
]
}
});
// console.log('wrapper.html: ', wrapper.html());
// Assert
const addressVehiclesQuestion = wrapper.find('address-vehicles-question-stub');
expect(addressVehiclesQuestion).toBe;
});
});

View file

@ -0,0 +1,243 @@
<template>
<Form
@submit="onSubmit"
@invalid-submit="onInvalidSubmit"
ref="theForm"
v-slot="{ meta }"
>
<div class="page-container-grouped-styles">
<loadingModal ref="loadingModal"/>
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<vehicleBanner cmsWidgetName="VehicleBannerWidget" :displayGenericVehicleImage="false" />
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<alert
ref="alertFoundMultipleVehicles"
class="my-5"
alertClass="alert-warning"
:manualHeadline="AlertFoundMultipleVehiclesHeader"
manualCopy=""
v-bind:isDismissible="false"
/>
<addressVehiclesQuestion
ref="addressVehiclesQuestion"
cmsWidgetName="VehicleConfirmationQuestion"
:vehicles="VehiclesForQuestions"
validationRules="vehicle-required"
v-model="selectedVehicleVin"
:isCarIdDifferent="isCarIdDifferent"
/>
<div class="alert-provide-vin my-3" v-if="splitAlertProvideVinBodyForLink.length">
<span v-for="copy in splitAlertProvideVinBodyForLink" :key="copy">
<span v-if="copy.includes('routerLink:')" class="text-body">
<router-link :to="{query: {fmgPage: `${copy.split(':')[1].split(',')[0]}`}, name: 'root'}">{{ copy.split(':')[1].split(',')[1] }}</router-link>
</span>
<span v-else class="m-0 text-body" v-html="copy"></span>
</span>
</div>
<funnelFooter
cmsWidgetName="FunnelFooterWidget"
ref="funnelFooter"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction"
/>
</div>
</Form>
</template>
<script>
// Components
import funnelHeader from "@/common-components/funnel-header/funnel-header";
import funnelFooter from "@/common-components/funnel-footer/funnel-footer";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import alert from "@/ux-components/alert/alert";
import addressVehiclesQuestion from "@/layouts/address-vehicles/address-vehicles-question/address-vehicles-question";
import loadingModal from '@/common-components/loading-modal/loading-modal.vue';
// Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import store from "@/store";
import baseMixin from "@/mixins/base-mixin.js";
import { storeActions } from "@/constants/store-actions";
import { storeMutations } from "@/constants/store-mutations";
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import { errorMessages } from "@/constants/error-messages";
import { required } from "@/helpers/validation-rules";
import { Form, defineRule } from "vee-validate";
import { navigateAfterSaveToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { isGlassAvailableForCarId } from "@/helpers/damage-helper";
// DEFINE VALIDATION RULES
defineRule("vehicle-required", required(errorMessages.VEHICLE_REQUIRED));
export default {
name: "address-vehicles",
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
// Settle promises and get results
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
});
},
props: {
validationRules: String,
},
data() {
return {
selectedVehicleVin: null,
isCarIdDifferent: false,
isSelectedGlassAvailableForVehicle: true,
};
},
computed: {
vehicleCount() {
return this.VehiclesForQuestions.length;
},
AlertFoundMultipleVehiclesHeader() {
let text = this.getCmsContent("FoundMultipleVehicles", "HeadlineText").replaceAll("{custom:vehicleCount}", this.vehicleCount);
return text;
},
AlertProvideVinBody() {
return this.getCmsContent("ProvideVinAlert", "BodyText");
},
splitAlertProvideVinBodyForLink() {
// Splits content when brackets are found in text so that text can be looped through and router-link can be injected when needed
return this.AlertProvideVinBody.split(/{(.*?)}/g);
},
VehiclesForQuestions() {
const vehiclesData = this.VehiclesFromApi;
// Map API result data, to address-vehicles data structure
const mappedData = vehiclesData.map((v) => {
const maskSymbol = "X";
const vinStart = maskSymbol.repeat(v.vin.length-4);
const vinEnd = v.vin.substring(v.vin.length-4);
return {
vin: v.vin,
vehicle: v.vehicle,
Text: v.vehicle.year + " " + v.vehicle.make + " " + v.vehicle.model,
Name: v.vin,
SubText: "VIN " + vinStart + vinEnd,
};
});
return mappedData;
},
VehiclesFromApi() {
return store.getters.pageData(fmgPageValues.ADDRESS_VEHICLES);
},
selectedVehicle() {
return this.VehiclesForQuestions.find( ({ vin }) => vin === this.selectedVehicleVin[0] );
},
},
methods: {
arePagePrerequisitesValid() {
if (
store.getters.order.vehicle.carId
&& store.getters.order.serviceLocation.zipCode
&& store.getters.order.customer.emailAddress
&& store.getters.pageData(fmgPageValues.ADDRESS_VEHICLES)
) {
return true;
}
return false;
},
backButtonAction() {
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
},
async forwardButtonAction() {
const vinLookup = await this.lookupVin(this.selectedVehicle.vin).catch(() => {
this.$refs.funnelFooter.removeLoader();
return;
});
this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId(vinLookup.data.carId);
this.updateCustomerInfo(this.selectedVehicle.vin, this.selectedVehicle.vehicle);
this.navigateForward();
},
navigateForward() {
if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) {
this.$router.navigateAfterSave(
this.navigationScenarios.CLICKED_FORWARD,
this.$route,
{},
{ displayVehicleChangeAlert: true },
);
return;
} else {
this.$refs.loadingModal.showModal();
navigateAfterSaveToHeritageFunnel(this.$route);
return;
}
},
lookupVin(vin) {
return baseMixin.methods.dispatchStoreAction(
storeActions.LOOKUP_VEHICLE_BY_VIN,
{ vin }
);
},
resetDependentState() { // needed because navigateAfterSaveToHeritageFunnel calls it
store.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
},
updateCustomerInfo(vin, vehicle) {
if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) {
store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
}
store.commit(storeMutations.UPDATE_VEHICLE_VIN, vin);
store.commit(storeMutations.UPDATE_YEAR, vehicle.year);
store.commit(storeMutations.UPDATE_MAKE, vehicle.make);
store.commit(storeMutations.UPDATE_MODEL, vehicle.model);
store.commit(storeMutations.UPDATE_STYLE, vehicle.style);
store.commit(storeMutations.UPDATE_CAR_ID, vehicle.carId);
store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, vehicle.category);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, vehicle.imageUrl);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, vehicle.imageVifNumber);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, vehicle.imageColor);
},
},
watch: {
selectedVehicleVin(vehicleVin) {
// 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}`);
},
},
components: {
Form,
funnelHeader,
vehicleBanner,
funnelSubHeader,
alert,
funnelFooter,
addressVehiclesQuestion,
loadingModal,
},
};
</script>
<style lang="scss">
.alert-provide-vin {
font-size: 0.875rem;
line-height: 1.4;
a {
text-underline-offset: .2em;
line-height: inherit;
}
}
</style>

View file

@ -8,28 +8,69 @@
<div class="fade-on-route-transition sub-container make-tall"> <div class="fade-on-route-transition sub-container make-tall">
<div class="row my-2"> <div class="row my-2">
<div class="col"> <div class="col">
<textboxQuestion cmsWidgetName="LicensePlateNumber" v-model="licensePlate" isRequired inputId="license_plate" validationRules="license-plate-required" /> <textboxQuestion
cmsWidgetName="LicensePlateNumber"
v-model="licensePlate"
isRequired
inputId="license_plate"
validationRules="license-plate-required"
/>
</div> </div>
</div> </div>
<div class="row my-2"> <div class="row my-2">
<div class="col"> <div class="col">
<textboxQuestion cmsWidgetName="RegistrationZip" v-model="registrationZip" inputId="zip" mask="#####" validationRules="zip-required" /> <textboxQuestion
cmsWidgetName="RegistrationZip"
v-model="registrationZip"
inputId="zip"
mask="#####"
validationRules="zip-required"
/>
</div> </div>
</div> </div>
<div class="row my-2"> <div class="row my-2">
<div class="col"> <div class="col">
<textboxQuestion cmsWidgetName="EmailAddress" v-model="email" inputId="email" validationRules="email-address-required|email-address-format" /> <textboxQuestion
cmsWidgetName="EmailAddress"
v-model="email"
inputId="email"
validationRules="email-address-required|email-address-format"
semiAggressiveValidation
/>
</div> </div>
</div> </div>
<alert class="my-3" :manualHeadline="NoServiceZipHeader" :manualCopy="NoServiceZipBody" v-if="!isRegistrationZipServicable && isVinValid && !isCarIdDifferent" alertClass="alert-danger" /> <alert
class="my-3"
:manualHeadline="NoServiceZipHeader"
:manualCopy="NoServiceZipBody"
v-if="!isRegistrationZipServicable && isVinValid && !isCarIdDifferent"
alertClass="alert-danger"
/>
<div class="row my-2"> <div class="row my-2">
<div class="col"> <div class="col">
<textboxQuestion v-if="!isRegistrationZipServicable" cmsWidgetName="ServiceZip" v-model="serviceZip" inputId="serviceZip" validationRules="zip-required" /> <textboxQuestion
v-if="!isRegistrationZipServicable"
cmsWidgetName="ServiceZip"
v-model="serviceZip"
inputId="serviceZip"
validationRules="zip-required|zip-format"
/>
</div> </div>
</div> </div>
<alert class="my-3" cmsWidgetName="NoMatchAlertWidget" v-if="!isVinValid" /> <alert class="my-3" cmsWidgetName="NoMatchAlertWidget" v-if="!isVinValid" />
<alert class="my-3" :manualHeadline="MatchedDifferentVehicleAlertHeader" :manualCopy="MatchedDifferentVehicleAlertBody" v-if="isCarIdDifferent" alertClass="alert-warning" /> <alert class="my-3"
<funnelFooter ref="funnelFooter" cmsWidgetName="FunnelFooterWidget" :isForwardActionDisabled="!meta.valid" @back-clicked="backButtonAction" @ForwardClicked="forwardButtonAction" /> :manualHeadline="MatchedDifferentVehicleAlertHeader"
:manualCopy="MatchedDifferentVehicleAlertBody"
v-if="isCarIdDifferent"
alertClass="alert-warning"
/>
<funnelFooter
ref="funnelFooter"
cmsWidgetName="FunnelFooterWidget"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction"
/>
</div> </div>
</div> </div>
</Form> </Form>
@ -46,25 +87,25 @@ import alert from "@/ux-components/alert/alert";
import textboxQuestion from "@/common-components/textbox-question/textbox-question"; import textboxQuestion from "@/common-components/textbox-question/textbox-question";
import loadingModal from '@/common-components/loading-modal/loading-modal.vue'; import loadingModal from '@/common-components/loading-modal/loading-modal.vue';
// Supporting files // Supporting files
import { import {
fetchCmsContentForPage fetchCmsContentForPage
} from "@/helpers/cms-content-helper"; } from "@/helpers/cms-content-helper";
import { import {
settleAllPromises settleAllPromises
} from "@/helpers/layout-helper"; } from "@/helpers/layout-helper";
import store from "@/store"; import store from "@/store";
import baseMixin from "@/mixins/base-mixin.js"; import baseMixin from "@/mixins/base-mixin.js";
import { import {
storeActions storeActions
} from "@/constants/store-actions"; } from "@/constants/store-actions";
import { import {
storeMutations storeMutations
} from "@/constants/store-mutations"; } from "@/constants/store-mutations";
import { import {
errorMessages errorMessages
} from "@/constants/error-messages"; } from "@/constants/error-messages";
import { import {
navigateAfterSaveToHeritageFunnel navigateAfterSaveToHeritageFunnel
} from "@/helpers/heritage-integration/navigation-helper"; } from "@/helpers/heritage-integration/navigation-helper";
import { import {
getDamageString, getDamageString,
@ -92,7 +133,7 @@ defineRule(
defineRule( defineRule(
"email-address-format", "email-address-format",
regex( regex(
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9_\-.]+).([a-zA-Z]{2,})$/, /^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9_\-.]+)\.([a-zA-Z]{2,})$/,
errorMessages.EMAIL_ADDRESS_FORMAT errorMessages.EMAIL_ADDRESS_FORMAT
) )
); );
@ -256,7 +297,7 @@ export default {
vinLookup.data.vehicle, vinLookup.data.vehicle,
zipValidation.data.state zipValidation.data.state
); );
this.navigateForward(); this.navigateForward();
}, },
navigateForward() { navigateForward() {
@ -281,10 +322,7 @@ export default {
}); });
}, },
lookupVin(plate, state) { lookupVin(plate, state) {
return baseMixin.methods.dispatchStoreAction( return baseMixin.methods.dispatchStoreAction(storeActions.LOOKUP_VIN_BY_PLATE,{ licensePlate: plate, licenseState: state }, false);
storeActions.LOOKUP_VIN_BY_PLATE,
{ licensePlate: plate, licenseState: state }
);
}, },
updateCustomerInfo(vin, vehicleInfo, registrationState) { updateCustomerInfo(vin, vehicleInfo, registrationState) {
if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) { if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) {
@ -335,4 +373,4 @@ export default {
loadingModal, loadingModal,
}, },
}; };
</script> </script>

View file

@ -87,8 +87,6 @@ import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages"; import { errorMessages } from "@/constants/error-messages";
import { damageLocationsCms } from "@/constants/damage-locations-cms.js"; import { damageLocationsCms } from "@/constants/damage-locations-cms.js";
import { damageLocationsSelected } from "@/constants/damage-locations-selected.js"; import { damageLocationsSelected } from "@/constants/damage-locations-selected.js";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { queryStrings } from "@/constants/query-strings";
import store from "@/store"; import store from "@/store";
import baseMixin from "@/mixins/base-mixin"; import baseMixin from "@/mixins/base-mixin";

View file

@ -10,7 +10,7 @@
<alert <alert
class="my-3" class="my-3"
cmsWidgetName="NoReplacementAvailableError" cmsWidgetName="NoReplacementAvailableError"
v-show="showNoReplacementAvailableError" v-if="showNoReplacementAvailableError"
alertClass="alert-danger" alertClass="alert-danger"
:isDismissible="false" :isDismissible="false"
/> />
@ -32,7 +32,7 @@
<alert <alert
class="my-3" class="my-3"
cmsWidgetName="SplitSingleConflict" cmsWidgetName="SplitSingleConflict"
v-show="hasSplitSingleConflict" v-if="hasSplitSingleConflict"
alertClass="alert-danger" alertClass="alert-danger"
:isDismissible="false" :isDismissible="false"
/> />

View file

@ -88,6 +88,10 @@ export default {
store.commit(storeMutations.UPDATE_STYLE, null); store.commit(storeMutations.UPDATE_STYLE, null);
store.commit(storeMutations.UPDATE_CAR_ID, null); store.commit(storeMutations.UPDATE_CAR_ID, null);
store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null); store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
store.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
// Invokes // Invokes
store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES); store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);

View file

@ -88,6 +88,10 @@ export default {
store.commit(storeMutations.UPDATE_STYLE, null); store.commit(storeMutations.UPDATE_STYLE, null);
store.commit(storeMutations.UPDATE_CAR_ID, null); store.commit(storeMutations.UPDATE_CAR_ID, null);
store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null); store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
store.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
// Invokes // Invokes
store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES); store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);

View file

@ -101,6 +101,7 @@ export default {
store.commit(storeMutations.UPDATE_IS_REPAIR, null); store.commit(storeMutations.UPDATE_IS_REPAIR, null);
store.commit(storeMutations.UPDATE_NUMBER_OF_CHIPS, null); store.commit(storeMutations.UPDATE_NUMBER_OF_CHIPS, null);
store.commit(storeMutations.UPDATE_GLASS_TO_REPLACE, []); store.commit(storeMutations.UPDATE_GLASS_TO_REPLACE, []);
store.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
}, },
}, },

View file

@ -107,8 +107,11 @@ export default {
store.commit(storeMutations.UPDATE_STYLE, null); store.commit(storeMutations.UPDATE_STYLE, null);
store.commit(storeMutations.UPDATE_CAR_ID, null); store.commit(storeMutations.UPDATE_CAR_ID, null);
store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null); store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
store.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
// Invokes // Invokes
store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES); store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
store.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES); store.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);

View file

@ -41,6 +41,9 @@ export default {
font-size: .875rem; font-size: .875rem;
color: $gray-600; color: $gray-600;
} }
li {
line-height: 26px;
}
a { a {
font-size: .875rem; font-size: .875rem;
} }
@ -70,6 +73,7 @@ export default {
overflow: hidden; overflow: hidden;
opacity: 0; opacity: 0;
img { img {
max-width: 420px;
width: 117%; width: 117%;
height: auto; height: auto;
} }

View file

@ -14,7 +14,7 @@
/> />
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" /> <funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<div class="fade-on-route-transition sub-container make-tall"> <div class="fade-on-route-transition sub-container make-tall">
<div class="row my-2"> <div class="row mt-2">
<div class="col"> <div class="col">
<textboxQuestion <textboxQuestion
cmsWidgetName="VinNumber" cmsWidgetName="VinNumber"
@ -27,7 +27,7 @@
/> />
</div> </div>
</div> </div>
<div class="row my-2"> <div class="row mb-2">
<div class="col"> <div class="col">
<vinInformation /> <vinInformation />
</div> </div>
@ -54,18 +54,19 @@
isRequired isRequired
disableAutoFill disableAutoFill
validationRules="email-address-required|email-address-format" validationRules="email-address-required|email-address-format"
semiAggressiveValidation
/> />
</div> </div>
</div> </div>
<alert <alert
class="my-3" class="my-4"
v-model="customAlertData" v-model="customAlertData"
v-if="noMatchAlert" v-if="noMatchAlert"
alertClass="alert-warning" alertClass="alert-warning"
cmsWidgetName="NoMatchAlertWidget" cmsWidgetName="NoMatchAlertWidget"
/> />
<alert <alert
class="my-3" class="my-4"
:manualHeadline="PerfectMatchNewVinAlertReadOnlyHeader" :manualHeadline="PerfectMatchNewVinAlertReadOnlyHeader"
:manualCopy="PerfectMatchNewVinAlertReadOnlyBody" :manualCopy="PerfectMatchNewVinAlertReadOnlyBody"
v-model="customAlertData" v-model="customAlertData"
@ -73,15 +74,15 @@
alertClass="alert-success" alertClass="alert-success"
/> />
<alert <alert
class="my-3" class="my-4"
:manualHeadline="MatchedDifferentVehicleAlertHeader" :manualHeadline="MatchedDifferentVehicleAlertHeader"
:manualCopy="MatchedDifferentVehicleAlertBody" :manualCopy="MatchedDifferentVehicleAlertBody"
v-model="customAlertData" v-model="customAlertData"
v-if="isCarIdDifferent" v-if="isCarIdDifferent && !vinNotFound && !perfectMatchNewVinAlert"
alertClass="alert-warning" alertClass="alert-warning"
/> />
<alert <alert
class="my-3" class="my-4"
:manualHeadline="NoServiceZipHeader" :manualHeadline="NoServiceZipHeader"
:manualCopy="NoServiceZipBody" :manualCopy="NoServiceZipBody"
v-model="customAlertData" v-model="customAlertData"
@ -89,18 +90,19 @@
alertClass="alert-danger" alertClass="alert-danger"
/> />
<alert <alert
class="my-3" class="my-4"
v-model="customAlertData" v-model="customAlertData"
v-if="vinNotFound" v-if="vinNotFound"
alertClass="alert-warning" alertClass="alert-danger"
cmsWidgetName="VinNotFound" cmsWidgetName="VinNotFound"
/> />
<alert <alert
class="my-3" class="my-4"
:manualHeadline="PerfectMatchNewVinAlertHeader"
:manualCopy="PerfectMatchNewVinAlertBody"
v-model="customAlertData" v-model="customAlertData"
v-if="perfectMatchNewVinAlert" v-if="perfectMatchNewVinAlert && !noServiceZip && !vinNotFound && meta.valid"
alertClass="alert-success" alertClass="alert-success"
cmsWidgetName="PerfectMatchNewVinAlert"
/> />
<funnelFooter <funnelFooter
cmsWidgetName="FunnelFooterWidget" cmsWidgetName="FunnelFooterWidget"
@ -139,7 +141,10 @@ import { Form, defineRule } from "vee-validate";
import { navigateAfterSaveToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; import { navigateAfterSaveToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
// DEFINE VALIDATION RULES // DEFINE VALIDATION RULES
defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED)); defineRule(
"zip-required",
required(errorMessages.SERVICE_ZIP_REQUIRED)
);
defineRule( defineRule(
"email-address-required", "email-address-required",
required(errorMessages.EMAIL_ADDRESS_REQUIRED) required(errorMessages.EMAIL_ADDRESS_REQUIRED)
@ -147,11 +152,14 @@ defineRule(
defineRule( defineRule(
"email-address-format", "email-address-format",
regex( regex(
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9_\-.]+).([a-zA-Z]{2,})$/, /^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9_\-.]+)\.([a-zA-Z]{2,})$/,
errorMessages.EMAIL_ADDRESS_FORMAT errorMessages.EMAIL_ADDRESS_FORMAT
) )
); );
defineRule("vin-required", required(errorMessages.VIN_REQUIRED)); defineRule(
"vin-required",
required(errorMessages.VIN_REQUIRED)
);
defineRule( defineRule(
"vin-format", "vin-format",
regex(/^[A-HJ-NPR-Z0-9]{17}$/, errorMessages.VIN_FORMAT) regex(/^[A-HJ-NPR-Z0-9]{17}$/, errorMessages.VIN_FORMAT)
@ -195,9 +203,18 @@ export default {
mounted() { mounted() {
this.attachCustomEvents(); this.attachCustomEvents();
}, },
watch: {
vin() {
this.$refs.funnelFooter.updateButtonText(
this.getCmsContent("FunnelFooterWidget", "ForwardButtonText")
);
}
},
computed: { computed: {
perfectMatchNewVinAlert() { perfectMatchNewVinAlert() {
return this.vinPopulatedOnPageLoad && this.vin === this.getVinFromStore(); const isVinPerfectMatch = this.vinPopulatedOnPageLoad && this.vin === this.getVinFromStore();
this.updateIsCarIdDifferent(isVinPerfectMatch);
return isVinPerfectMatch;
}, },
MatchedDifferentVehicleAlertHeader(){ MatchedDifferentVehicleAlertHeader(){
const text = this.getCmsContent("MatchedDifferentVehicle", const text = this.getCmsContent("MatchedDifferentVehicle",
@ -268,8 +285,18 @@ export default {
); );
}); });
}, },
updateIsCarIdDifferent(isVinPerfectMatch){
if (isVinPerfectMatch) {
this.isCarIdDifferent = false;
}
},
backButtonAction() { backButtonAction() {
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route); if (store.getters.vehicle.vin) {
this.$router.navigate(this.navigationScenarios.CLICKED_BACK_WITH_VIN, this.$route);
}
else {
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
}
}, },
async forwardButtonAction() { async forwardButtonAction() {
const zipValidation = this.validateZip(this.zip); const zipValidation = this.validateZip(this.zip);

View file

@ -42,6 +42,9 @@ export default {
el && el.focus(); el && el.focus();
} }
}, },
getFooterInfoBoxHeight() {
return document.querySelector(".footer #infoBox").offsetHeight;
}
}, },
computed: { computed: {
storeActions() { storeActions() {

View file

@ -214,13 +214,6 @@ function navigateToUrl(url, optionalQuery = {}) {
externalUrl.searchParams.append(queryKey, optionalQuery[queryKey]); externalUrl.searchParams.append(queryKey, optionalQuery[queryKey]);
} }
/////////////////////////////////////////////////////
// TEMP CODE FOR TESTING WITH SPECIFIC EXPERIMENTS //
/////////////////////////////////////////////////////
if (externalUrl.search.indexOf("corid=") != -1)
externalUrl.search = externalUrl.search + '&experiments=CollectEmailOnQuote=CollectEmailOnQuote_V1=YesCollectEmail_TEST1=true,RemoveServiceAreaPage=ServAreaRemoval_V7=ServAreaNoRemove_V7_TEST=true,VINeducationV2=VINeducation_V2=NoShowVINmodalV2_CONTROL=true,ServicePackages=ServicePackages_V1=NoShowPackages_CONTROL=true,PhotoUploadRedesign=PhotoUploadRedesign_V1=CurrentPhotoUpload_CONTROL=true,ScheduleDetailsServiceType=ScheduleBeforeServiceType_V1=ServTypeThenSched_CONTROL=true';
///////////// END TEMP CODE /////////////////////////
window.location.assign(externalUrl); window.location.assign(externalUrl);
} }

View file

@ -107,6 +107,10 @@ const routingTable = [
{ {
scenario: navigationScenarios.CLICKED_FORWARD, scenario: navigationScenarios.CLICKED_FORWARD,
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE, destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_VIN,
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
} }
], ],
}, },
@ -139,8 +143,22 @@ const routingTable = [
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE, destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
}, },
], ],
}, },
{fmgPageValue: fmgPageValues.ESTIMATE, {
fmgPageValue: fmgPageValues.ADDRESS_VEHICLES,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationFmgPageValue: fmgPageValues.ADDRESS_LOOKUP,
},
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
},
],
},
{
fmgPageValue: fmgPageValues.ESTIMATE,
maps: [ maps: [
{ {
scenario: navigationScenarios.CLICKED_BACK, scenario: navigationScenarios.CLICKED_BACK,

View file

@ -15,7 +15,7 @@ const getDefaultState = () => {
model: null, model: null,
style: null, style: null,
carId: null, carId: null,
category: null, category: null,
vin: null, vin: null,
imageUrl: null, imageUrl: null,
imageVifNumber: null, imageVifNumber: null,
@ -31,6 +31,9 @@ const getDefaultState = () => {
}, },
}, },
serviceLocation: { serviceLocation: {
address: null,
city: null,
state: null,
zipCode: null, zipCode: null,
}, },
customer: { customer: {
@ -187,6 +190,10 @@ export const mutations = {
state.order.vehicle.style = null; state.order.vehicle.style = null;
state.order.vehicle.carId = null; state.order.vehicle.carId = null;
state.order.vehicle.category = null; state.order.vehicle.category = null;
state.order.vehicle.vin = null;
state.order.vehicle.imageUrl = null;
state.order.vehicle.imageVifNumber = null;
state.order.vehicle.imageColor = null;
}, },
resetDamageState(state) { resetDamageState(state) {
state.order.damage.isRepair = null; state.order.damage.isRepair = null;
@ -227,18 +234,39 @@ export const mutations = {
imageUrl: orderInformation.vehicle?.imageUrl, imageUrl: orderInformation.vehicle?.imageUrl,
imageVifNumber: orderInformation.vehicle?.imageVifNumber, imageVifNumber: orderInformation.vehicle?.imageVifNumber,
imageColor: orderInformation.vehicle?.imageVifColor, imageColor: orderInformation.vehicle?.imageVifColor,
registration: {
firstName: orderInformation.vehicle.registration.firstName,
lastName: orderInformation.vehicle.registration.lastName,
address: orderInformation.vehicle.registration.streetAddress,
city: orderInformation.vehicle.registration.city,
state: orderInformation.vehicle.registration.state,
zipCode: orderInformation.vehicle.registration.zipCode,
licensePlate: orderInformation.vehicle.registration.licensePlateNumber,
}
}); });
state.order.damage.glassToReplace = orderInformation.glassToReplace; state.order.damage.glassToReplace = orderInformation.damage.glassToReplace;
state.order.damage.isRepair = orderInformation.isRepair; state.order.damage.isRepair = orderInformation.damage.isRepair;
state.order.damage.numberOfChips = orderInformation.numberOfChips; state.order.damage.numberOfChips = orderInformation.damage.numberOfChips;
state.order.lineItems.glassParts = orderInformation.parts; state.order.lineItems.glassParts = orderInformation.parts;
state.order.accountNumber = orderInformation.accountNumber; state.order.accountNumber = orderInformation.accountNumber;
state.order.serviceLocation.zipCode = orderInformation.zipCode; state.order.serviceLocation.address = orderInformation.serviceLocation.streetAddress,
state.order.serviceLocation.city = orderInformation.serviceLocation.city,
state.order.serviceLocation.state = orderInformation.serviceLocation.state,
state.order.serviceLocation.zipCode = orderInformation.serviceLocation.zipCode;
state.order.payment.isInsurance = orderInformation.IsInsuranceOrder; state.order.payment.isInsurance = orderInformation.IsInsuranceOrder;
state.order.payment.insuranceCoverage.isVerified = orderInformation?.insuranceInfo.coverageVerified; state.order.payment.insuranceCoverage.isVerified = orderInformation?.insuranceInfo.coverageVerified;
state.order.customer.emailAddress = orderInformation.customer.emailAddress;
},
updateServiceLocationWithVehicleRegistration(state) {
state.order.serviceLocation.address = state.order.vehicle.registration.address;
state.order.serviceLocation.city = state.order.vehicle.registration.city;
state.order.serviceLocation.state = state.order.vehicle.registration.state;
state.order.serviceLocation.zipCode = state.order.vehicle.registration.zipCode;
} }
} }
@ -419,6 +447,9 @@ export const actions = {
context.commit(storeMutations.UPDATE_REFERRAL_DATE, referralDate); context.commit(storeMutations.UPDATE_REFERRAL_DATE, referralDate);
context.commit(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, referralCorrelationId); context.commit(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, referralCorrelationId);
}, },
updateServiceLocationWithVehicleRegistration(context) {
context.commit(storeMutations.UPDATE_SERVICE_LOCATION_WITH_VEHICLE_REGISTRATION);
},
logExperimentExposure(context, { userId, sessionKey, pageName, universeName }) { logExperimentExposure(context, { userId, sessionKey, pageName, universeName }) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
@ -502,6 +533,7 @@ export const actions = {
saveOrder(context) { saveOrder(context) {
const vehicle = context.getters.vehicle; const vehicle = context.getters.vehicle;
const damage = context.getters.damage; const damage = context.getters.damage;
const order = context.state.order;
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.SaveOrder.method, method: endpoints.SaveOrder.method,
@ -513,14 +545,34 @@ export const actions = {
make: vehicle.make, make: vehicle.make,
model: vehicle.model, model: vehicle.model,
style: vehicle.style, style: vehicle.style,
vin: vehicle.vin vin: vehicle.vin,
registration: {
firstName: vehicle.registration.firstName,
lastName: vehicle.registration.lastName,
streetAddress: vehicle.registration.address,
city: vehicle.registration.city,
state: vehicle.registration.state,
zipCode: vehicle.registration.zipCode,
licensePlateNumber: vehicle.registration.licensePlate,
},
}, },
numberOfChips: damage.numberOfChips, damage: {
zipCode: 43215, // TODO CSR-416, should not be hardcoded (state.order.serviceLocation.zipCode) numberOfChips: damage.numberOfChips,
glassToReplace: damage.glassToReplace, glassToReplace: damage.glassToReplace,
referralNumber: context.state.order.referralNumber, isRepair: damage.isRepair
referralDate: context.state.order.referralDate, },
accountNumber: context.state.order.accountNumber customer: {
emailAddress: order.customer.emailAddress,
},
serviceLocation: {
streetAddress: order.serviceLocation.address,
city: order.serviceLocation.city,
state: order.serviceLocation.state,
zipCode: order.serviceLocation.zipCode
},
referralNumber: order.referralNumber,
referralDate: order.referralDate,
accountNumber: order.accountNumber
}, },
}); });
}, },

View file

@ -215,14 +215,19 @@ describe("Mutations", () => {
model: "ILX", model: "ILX",
style: "4 DOOR SEDAN", style: "4 DOOR SEDAN",
carId: "C0000001", carId: "C0000001",
category: "CAR" category: "CAR",
registration: {}
},
damage: {
glassToReplace: ["Windshield"],
isRepair: false,
numberOfChips: 0,
}, },
glassToReplace: ["Windshield"],
isRepair: false,
numberOfChips: 0,
parts: [], parts: [],
accountNumber: "123456789", accountNumber: "123456789",
insuranceInfo: {} insuranceInfo: {},
serviceLocation: {},
customer: {}
}); });
// Assert // Assert
@ -562,11 +567,16 @@ describe("Actions", () => {
const context = state; const context = state;
context.getters = { context.getters = {
vehicle: {}, vehicle: {
registration: {}
},
damage: {}, damage: {},
}; };
context.state = { context.state = {
order: {} order: {
serviceLocation: {},
customer: {}
}
}; };
globalMethods.callHttpClient.mockImplementation(() => { globalMethods.callHttpClient.mockImplementation(() => {

View file

@ -1,8 +1,9 @@
html { html {
.has-error { .has-error {
&.list-button, &.list-button,
&.list-card, &.list-card,
&.list-card.list-button { &.list-card.list-button {
border: none;
color: $red; color: $red;
input[type=checkbox]:focus + label, input[type=checkbox]:focus + label,
input[type=radio]:focus + label { input[type=radio]:focus + label {
@ -13,15 +14,16 @@ html {
} }
&:hover { &:hover {
box-shadow: 0px 0px 0px 4px $red-200; box-shadow: 0px 0px 0px 4px $red-200;
border-radius: 10px; border-radius: .5rem;
} }
label { label {
border: 1px solid $red; border: 1px solid $red;
border-radius: .5rem;
} }
label:hover { label:hover {
box-shadow: 0px 0px 0px 4px $red-200; box-shadow: 0px 0px 0px 4px $red-200;
border-radius: 10px; border-radius: 10px;
border: 1px solid $red; border: 1px solid $red;
} }
} }
&.list-button-horizontal { &.list-button-horizontal {
@ -51,12 +53,21 @@ html {
} }
&.textbox-question, &.textbox-question,
&.dropdown-question { &.dropdown-question {
input:hover {
box-shadow: 0px 0px 0px 4px $red-200;
border-radius: .5rem;
border: 1px solid $red;
}
input:focus {
box-shadow: 0 0 0 2.5px $red;
}
p { p {
color: $red; color: $red;
} }
input, input,
select { select {
border: 1px solid $red; border: 1px solid transparent;
box-shadow: 0 0 0 1px $red;
&:focus { &:focus {
border: 1px solid transparent; border: 1px solid transparent;
} }
@ -114,8 +125,6 @@ html {
color: $red; color: $red;
font-size: .875rem; font-size: .875rem;
font-weight: 500; font-weight: 500;
height: 1.5rem;
margin-top: .25rem !important;
} }
.form-test-invalid { .form-test-invalid {
@ -129,7 +138,6 @@ html {
&.btn.btn-primary:hover, &.btn.btn-primary:hover,
&.btn.btn-primary:focus, &.btn.btn-primary:focus,
&.btn.btn-primary:focus-visible { &.btn.btn-primary:focus-visible {
color: $gray !important;
background: $gray-200; background: $gray-200;
box-shadow: none; box-shadow: none;
} }

View file

@ -66,6 +66,7 @@ describe("alert.vue", () => {
const mockMixin = { const mockMixin = {
methods: { methods: {
getCmsContent: jest.fn() getCmsContent: jest.fn(),
getFooterInfoBoxHeight: jest.fn(()=> 80),
} }
} }

View file

@ -5,15 +5,17 @@
:class="[isDismissible ? 'alert-dismissible' : '', this.alertClass]" :class="[isDismissible ? 'alert-dismissible' : '', this.alertClass]"
> >
<p class="m-0 fw-bold alert-heading">{{ alertHeadline }}</p> <p class="m-0 fw-bold alert-heading">{{ alertHeadline }}</p>
<p v-if="splitAlertCopyForLink.length"> <template v-for="paragraph in splitAlertCopyForParagraphTag" :key="paragraph">
<template v-for="copy in splitAlertCopyForLink" :key="copy"> <p class="m-0 text-body small" v-if="!doesCopyContainRouterLink(paragraph)" v-html="paragraph"></p>
<span v-if="copy.includes('routerLink:')" class="m-0 text-body"> <p class="m-0 text-body small" v-else>
<router-link :to="{query: {fmgPage: `${copy.split(':')[1].split(',')[0]}`}, name: 'root'}">{{ copy.split(':')[1].split(',')[1] }}</router-link> <template v-for="copy in splitCopyForRouterLink(paragraph)" :key="copy">
</span> <span v-if="!doesCopyContainRouterLink(copy)" v-html="copy"></span>
&nbsp;<span v-else class="m-0 text-body" v-html="copy"></span>&nbsp; <span v-else>
</template> <router-link :to="{query: {fmgPage: `${getRouterLinkRouteFromCopy(copy)}`}, name: 'root'}">{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link>
</p> </span>
<p v-else class="m-0 text-body" v-html="alertCopy"></p> </template>
</p>
</template>
<button <button
type="button" type="button"
class="btn-close p-2" class="btn-close p-2"
@ -41,7 +43,7 @@ export default {
isDismissible: Boolean, isDismissible: Boolean,
/* /*
alertClass class names: alertClass class names:
alert-sucess (green) alert-success (green)
alert-danger (red) alert-danger (red)
alert-warning (yellow) alert-warning (yellow)
alert-info (blue) alert-info (blue)
@ -50,6 +52,10 @@ export default {
cmsWidgetName: String, cmsWidgetName: String,
manualHeadline: String, manualHeadline: String,
manualCopy: String, manualCopy: String,
shouldScrollToOnMount: {
type: Boolean,
default: true
},
}, },
computed: { computed: {
alertHeadline(){ alertHeadline(){
@ -58,11 +64,60 @@ export default {
alertCopy(){ alertCopy(){
return this.cmsWidgetName ? this.getCmsContent(this.cmsWidgetName, 'BodyText') : this.manualCopy; return this.cmsWidgetName ? this.getCmsContent(this.cmsWidgetName, 'BodyText') : this.manualCopy;
}, },
splitAlertCopyForLink(){ splitAlertCopyForParagraphTag(){
// Splits content when brackets are found in text so that text can be looped through and router-link can be injected when needed // splits the alertCopy on <p ... > (with or without attributes) and </p>
return this.alertCopy.split(/{(.*?)}/g); // filter removes empty strings that are a result of string.split with regex
} return this.alertCopy.split(/(?:<p(?:.*?)>)|(?:<\/p>)/g).filter(paragraph => paragraph !== "");
},
}, },
methods: {
doesCopyContainRouterLink(copy) {
return copy.includes('routerLink:');
},
splitCopyForRouterLink(copy){
// splits copy on { ... } such as {routerlink: ...}
return copy.split(/{(.*?)}/g);
},
getRouterLinkRouteFromCopy(copy){
// sample input: {routerLink:estimate,provide your VIN}
// first split would return 'estimate,provide your VIN'
// second split would return 'estimate'
return copy.split(':')[1].split(',')[0];
},
getRouterLinkDisplayTextFromCopy(copy){
// sample input: {routerLink:estimate,provide your VIN}
// first split would return 'estimate,provide your VIN'
// second split would return 'provide your VIN'
return copy.split(':')[1].split(',')[1];
},
ensureAlertIsInViewPort() {
if (this.shouldScrollToOnMount && this.$el.style.display != 'none') {
var footerHeight = this.getFooterInfoBoxHeight();
if (!this.isAlertInViewport(footerHeight)) {
this.scrollContainerToAlert(footerHeight);
}
}
},
isAlertInViewport(footerHeight) {
const rect = this.$el.getBoundingClientRect();
return (
rect.top >= 0 &&
// remove footerHeight from window height to avoid items being hidden behind footer
rect.bottom <= (window.innerHeight - footerHeight || document.documentElement.clientHeight - footerHeight)
);
},
scrollContainerToAlert(footerHeight) {
// alert position on page + height of alert + footer height
var scrollToHeight = this.$el.scrollHeight + this.$el.offsetHeight + footerHeight;
// find the div wrapped by the form element - this is the scrollable container
// should be a more future-proof selector in case of CSS class changes
var pageContainerScrollable = document.querySelector('form > div');
pageContainerScrollable.scrollTo(0, scrollToHeight);
},
},
mounted() {
this.ensureAlertIsInViewPort();
}
}; };
</script> </script>
@ -131,10 +186,6 @@ export default {
& p { & p {
font-size: .875rem; font-size: .875rem;
margin-bottom: 0.25rem !important; margin-bottom: 0.25rem !important;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
} }
} }
</style> </style>

View file

@ -1,6 +1,6 @@
<template> <template>
<div <div
class="list-group list-button-horizontal d-flex flex-column w-100 mb-2" class="list-group list-button-horizontal d-flex flex-column w-100"
:class="[(errors.length > 0 || hasError) ? 'has-error' : '']" :class="[(errors.length > 0 || hasError) ? 'has-error' : '']"
@keyup.space="triggerButton()" @keyup.space="triggerButton()"
@keyup.up="handleKeyupArrow()" @keyup.up="handleKeyupArrow()"