Merge pull request #1184 from Safelite/feature/CSR-1410

Feature/csr 1410
This commit is contained in:
Leah Schumann 2023-06-27 09:31:26 -04:00 committed by GitHub
commit 81c0561dcd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 262 additions and 248 deletions

View file

@ -90,10 +90,12 @@ export default {
openModal() {
const modal = Modal.getOrCreateInstance(document.getElementById(this.modalId));
modal.show();
this.$emit("isModalOpened", true);
},
closeModal() {
const modal = Modal.getInstance(document.getElementById(this.modalId));
modal.hide();
this.$emit("isModalOpened", false);
},
},
computed: {

View file

@ -7,7 +7,6 @@ import { mount, shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { storeMutations } from "@/constants/store-mutations";
import store from "@/store";
import { createImportSpecifier } from "typescript";
let autocompleteElement;
describe("address-questions.vue", () => {
@ -134,22 +133,19 @@ describe("address-questions.vue", () => {
});
test("Should set this.showAddressFields to true when the model is prepopulated", async () => {
// Arrange
// Act
const newAddressModel = {
streetAddress: "foo",
city: "foo",
state: "foo",
zipCode: "55555",
};
const wrapper = shallowMount(addressQuestions, {
propsData: {
modelValue: newAddressModel,
// Arrange / Act
const { wrapper } = setupMocks({
props: {
modelValue: {
streetAddress: "foo",
city: "foo",
state: "foo",
zipCode: "55555",
},
},
});
// Act
wrapper.vm.setupAddressLookup();
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.showAddressFields).toBe(true);

View file

@ -122,13 +122,15 @@ export default {
},
data() {
return {
showAddressFields: false,
displayVerificationWarning: false,
displayNoMatchWarning: false,
alertHeadlineVerificationWarning: "",
alertCopyVerificationWarning: "",
alertHeadlineNoMatchWarning: "",
alertCopyNoMatchWarning: "",
autocomplete: null,
autocompleteListener: null,
showAddressFields: false,
matchFound: null, // null = no attempted match, true = match was found, false = match was not found
};
},
@ -203,182 +205,194 @@ export default {
return this.captureApartmentNumberOrBusinessName;
},
},
addressField1: {
get: function () {
return document.getElementById("autocomplete");
},
},
},
methods: {
setupAddressLookup() {
this.showAddressFields = false;
if (
this.addressModel.streetAddress &&
this.addressModel.city &&
this.addressModel.state &&
this.addressModel.zipCode
) {
this.showAddressFields = true;
return;
}
const addressField1 = document.getElementById("autocomplete");
const self = this;
loadGooglePlacesAutocompleteScript() {
// Load the Google Places Autocomplete script
const apiKey = applicationConfig.GOOGLE_PLACES_API_KEY;
this.$loadScript(
`https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places&callback=Function.prototype`
)
.then(() => {
// Script is loaded, initialize the autocomplete textbox
const autocomplete = new window.google.maps.places.Autocomplete(addressField1, {
componentRestrictions: { country: ["us"] },
fields: ["address_components"],
types: ["geocode"],
});
).then(() => {
// When loaded trigger the setup
this.initializeAutocomplete();
});
},
initializeAutocomplete() {
// Initialize the Google Places Autocomplete
this.autocomplete = new window.google.maps.places.Autocomplete(this.addressField1, {
componentRestrictions: { country: ["us"] },
fields: ["address_components"],
types: ["geocode"],
});
// Standard place_changed event handling
const autocompleteListener = window.google.maps.event.addListener(
autocomplete,
"place_changed",
fillInAddress
// Set up the Autocomplete place_changed event to call our method to fill in the address
this.autocompleteListener = window.google.maps.event.addListener(
this.autocomplete,
"place_changed",
this.fillInAddress
);
// When the Street Address textbox receives focus,
// append the search results list container to the bottom of the textbox
// and disable browser autofill
this.addressField1.addEventListener("focus", (e) => {
// 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);
}
// Unfortunately this is the only place we can set the autocomplete attribute without the
// Google Places object resetting it to "off" which does nothing to prevent browser autofill
this.addressField1.setAttribute("autocomplete", "do-not-autofill");
});
this.addressField1.addEventListener("keydown", (e) => {
const event = new Event("place_changed");
// When either of the two enter keys or the tab key are pressed
if (e.code === "Enter" || e.code === "NumpadEnter" || e.code === "Tab") {
// Grab the selected item
const selectedItem = document.querySelector(
".pac-container .pac-item-selected"
);
addressField1.addEventListener("focus", () => {
// Wrapping the addressField1 element in the Google Address Autocomplete object
// will cause "autocomplete='off'" which Chrome completely ignores. This event
// handler will set the value to something arbitrary so autofill doesn't work.
// https://stackoverflow.com/a/30976223
addressField1.setAttribute("autocomplete", "do-not-autofill");
if (selectedItem !== null) {
// If an item was selected then fill in the address with the selected item
// by triggering the "place_changed" event of the Autocomplete object
this.autocomplete.dispatchEvent(event);
} else {
// Otherwise fill-in the address using first item from the list.
this.fillInAddressUsingFirstItem();
}
} else {
return;
}
});
// 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);
}
});
this.addressField1.addEventListener("change", () => {
// If a match has been previously attempted then do nothing
if (this.matchFound !== null) {
return;
}
addressField1.addEventListener("keydown", (e) => {
const autocomplete = document.getElementById("autocomplete");
const event = new Event("place_changed");
// Get the address that the user clicked on (if any)
const clickedAddress = document.querySelector(".pac-container .pac-item:hover");
if (e.code === "Enter" || e.code === "NumpadEnter" || e.code === "Tab") {
const selectedItem = document.querySelector(
".pac-container .pac-item-selected"
);
if (selectedItem !== null) {
// Fill-in the address using selected item in the list.
autocomplete.dispatchEvent(event);
//fillInAddress(selectedItem.textContent);
} else {
// Fill-in the address using first item in the list.
fillInAddressUsingFirstItem();
// If the Street Address field changed without clicking (i.e. by pressing Tab, or clicking outside the field)
if (clickedAddress === null) {
// Fill-in the address using first item in the list.
this.fillInAddressUsingFirstItem();
}
});
},
fillInAddress(place) {
if (!place) {
place = this.autocomplete.getPlace();
}
if (place && place.address_components) {
this.matchFound = true;
const self = this;
this.$nextTick(function () {
self.showAddressFields = true;
for (const component of place.address_components) {
const componentType = component.types[0];
switch (componentType) {
case "street_number": {
self.addressModel.streetAddress = component.long_name;
break;
}
case "route": {
self.addressModel.streetAddress += " " + component.short_name;
break;
}
case "locality": {
self.addressModel.city = component.long_name;
break;
}
case "administrative_area_level_1": {
self.addressModel.state = component.short_name;
break;
}
case "postal_code": {
self.addressModel.zipCode = component.long_name;
break;
}
} else {
return;
}
});
addressField1.addEventListener("change", () => {
// If a match has been previously attempted then do nothing
if (self.matchFound !== null) {
return;
}
// Get the address that the user clicked on (if any)
const clickedAddress = document.querySelector(
".pac-container .pac-item:hover"
);
// If the Street Address field changed without clicking (i.e. by pressing Tab, or clicking outside the field)
if (clickedAddress === null) {
// Fill-in the address using first item in the list.
fillInAddressUsingFirstItem();
}
});
function fillInAddressUsingFirstItem() {
// Fill-in the address using first item in the list.
const item = document.querySelector(".pac-container .pac-item");
if (item != null) {
const firstResult = item.textContent;
const geocoder = new window.google.maps.Geocoder();
geocoder.geocode(
{
address: firstResult,
},
function (results, status) {
if (status === window.google.maps.GeocoderStatus.OK) {
fillInAddress(results[0]);
self.displayVerificationWarning = true;
}
}
);
} else {
self.matchFound = false;
}
}
function fillInAddress(place) {
if (!place) {
place = autocomplete.getPlace();
}
if (place && place.address_components) {
self.matchFound = true;
self.$nextTick(function () {
self.showAddressFields = true;
for (const component of place.address_components) {
const componentType = component.types[0];
switch (componentType) {
case "street_number": {
self.addressModel.streetAddress = component.long_name;
break;
}
case "route": {
self.addressModel.streetAddress +=
" " + component.short_name;
break;
}
case "locality": {
self.addressModel.city = component.long_name;
break;
}
case "administrative_area_level_1": {
self.addressModel.state = component.short_name;
break;
}
case "postal_code": {
self.addressModel.zipCode = component.long_name;
break;
}
}
}
// after showing the address fields, disable the address autocomplete
window.google.maps.event.removeListener(autocompleteListener);
window.google.maps.event.clearInstanceListeners(autocomplete);
addressField1.onchange = null;
const pacContainer = document.querySelector(".pac-container");
if (pacContainer) {
pacContainer.remove();
}
});
}
}
})
.catch(() => {
// Failed to fetch script
console.log("Unable to load Google Places API script");
// After filling in the address fields, disable the address autocomplete
this.unloadAutocomplete();
});
}
},
fillInAddressUsingFirstItem() {
// Fill-in the address using first item in the list.
const item = document.querySelector(".pac-container .pac-item");
if (item != null) {
const firstResult = item.textContent;
const geocoder = new window.google.maps.Geocoder();
const self = this;
geocoder.geocode(
{
address: firstResult,
},
function (results, status) {
if (status === window.google.maps.GeocoderStatus.OK) {
self.fillInAddress(results[0]);
self.displayVerificationWarning = true;
}
}
);
} else {
this.matchFound = false;
}
},
resetAlerts() {
this.displayVerificationWarning = false;
},
unloadAutocomplete() {
if (this.autocompleteListener && this.autocomplete) {
window.google.maps.event.removeListener(this.autocompleteListener);
this.autocompleteListener = null;
window.google.maps.event.clearInstanceListeners(this.autocomplete);
this.autocomplete = null;
this.addressField1.onchange = null;
const pacContainer = document.querySelector(".pac-container");
if (pacContainer) {
pacContainer.remove();
}
}
},
},
mounted() {
this.setupAddressLookup();
// If we already have a full address, show it
this.showAddressFields =
(this.addressModel.streetAddress ?? "") !== "" &&
(this.addressModel.city ?? "") !== "" &&
(this.addressModel.state ?? "") !== "" &&
(this.addressModel.zipCode ?? "") !== "";
if (!this.showAddressFields) {
this.loadGooglePlacesAutocompleteScript();
}
},
unmounted() {
this.unloadAutocomplete();
},
watch: {
matchFound: {
@ -398,7 +412,7 @@ export default {
this.addressModel.state = "";
this.addressModel.zipCode = "";
}
this.showAddressFields = true;
this.displayVerificationWarning = false;
// Only deep watch the Address Model after a failed match
@ -414,11 +428,6 @@ export default {
}
},
},
modelValue: {
handler() {
this.setupAddressLookup();
},
},
},
components: {
textboxQuestion,

View file

@ -301,12 +301,18 @@ export default {
);
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.email, false);
await this.dispatchStoreAction(
storeActions.SAVE_SERVICE_LOCATION,
{
zipCode: this.serviceZipCode,
address: "",
address2: "",
city: "",
state: resultMap.serviceZipValidationResponse.state,
zipCode: this.serviceZipCode,
zipCodeCtu: resultMap.serviceZipValidationResponse.zipCodeCtu,
appointmentType: "",
isVehicleProtected: null,
},
false
);

View file

@ -95,8 +95,10 @@ export default {
</script>
<style lang="scss">
.list-card img {
height: auto;
width: 3.417rem;
.appointment-type-question {
.list-card img {
height: auto;
width: 3.417rem;
}
}
</style>

View file

@ -32,24 +32,27 @@
:footerButtonText="modalFooterText"
:onModalOpenedCallback="onModalOpened"
:onModalClosedCallback="onModalClosed"
@isModalOpened="setModalStatus"
@footer-button-event="setMobileLocation">
<addressQuestions
ref="addressQuestions"
v-model="internalModel.addressQuestions"
captureApartmentNumberOrBusinessName="true"
preserveCityAndStateOnReset="true" />
<vehicleProtectedQuestion
ref="vehicleProtectedQuestion"
v-model="internalModel.isVehicleProtected"
cmsWidgetName="VehicleProtectedQuestionWidget" />
<textBlock cmsWidgetName="WorkspaceRequirementsWidget" typeStyle="caption" />
<alert
ref="alertInvalidZip"
v-if="displayInvalidZipAlert"
class="my-4"
cmsWidgetName="AlertInvalidZipWidget"
alertClass="alert-danger"
v-bind:isDismissible="false" />
<template v-if="isModalOpened">
<addressQuestions
ref="addressQuestions"
v-model="internalModel.addressQuestions"
captureApartmentNumberOrBusinessName="true"
preserveCityAndStateOnReset="true" />
<vehicleProtectedQuestion
ref="vehicleProtectedQuestion"
v-model="internalModel.isVehicleProtected"
cmsWidgetName="VehicleProtectedQuestionWidget" />
<textBlock cmsWidgetName="WorkspaceRequirementsWidget" typeStyle="caption" />
<alert
ref="alertInvalidZip"
v-if="displayInvalidZipAlert"
class="my-4"
cmsWidgetName="AlertInvalidZipWidget"
alertClass="alert-danger"
v-bind:isDismissible="false" />
</template>
</modal>
</div>
</transition>
@ -81,6 +84,8 @@ export default {
return {
internalModel: deepClone(this.modelValue),
displayInvalidZipAlert: false,
addressQuestionsKey: 0,
isModalOpened: false,
};
},
setup(props) {
@ -184,6 +189,9 @@ export default {
modalFooterText() {
return this.getCmsContent(this.modalWidgetName, "FooterText");
},
modal() {
return this.$refs[this.modalName];
},
addressModel: {
get: function () {
return this.modelValue.addressQuestions;
@ -192,48 +200,23 @@ export default {
},
methods: {
openModal() {
this.$refs[this.modalName].openModal();
},
closeModal() {
this.$refs[this.modalName].closeModal();
this.modal.openModal();
},
onModalOpened() {
this.internalModel = deepClone(this.modelValue);
},
setModalStatus(isOpened) {
this.isModalOpened = isOpened;
},
closeModal() {
this.modal.closeModal();
},
onModalClosed() {
this.displayInvalidZipAlert = false;
this.internalModel = deepClone(this.modelValue);
this.resetValidation();
},
resetComponent(updatedServiceZipCodeInfo) {
// Reset the validation form, setting the initial values
// for the state and zipCode to those that were entered
// on the service-zip-modal-question component
this.$refs[this.modalName].resetForm({
values: {
autocomplete: updatedServiceZipCodeInfo.streetAddress,
city: updatedServiceZipCodeInfo.city,
state: updatedServiceZipCodeInfo.state,
zipCode: updatedServiceZipCodeInfo.zipCode,
isVehicleProtected: updatedServiceZipCodeInfo.isVehicleProtected,
},
});
},
resetModalButtonStyle() {
this.$refs[this.modalName].resetButtonStyle();
},
resetValidation() {
this.$refs.addressQuestions.resetAlerts();
this.$refs[this.modalName].resetForm({
values: {
autocomplete: this.internalModel.addressQuestions.streetAddress,
city: this.internalModel.addressQuestions.city,
state: this.internalModel.addressQuestions.state,
zipCode: this.internalModel.addressQuestions.zipCode,
isVehicleProtected: this.internalModel.isVehicleProtected,
},
});
this.modal.resetButtonStyle();
},
async setMobileLocation() {
if (
@ -283,14 +266,6 @@ export default {
this.internalModel = deepClone(newValue);
this.handleChange(newValue);
this.resetComponent({
streetAddress: newValue.addressQuestions.streetAddress,
city: newValue.addressQuestions.city,
state: newValue.addressQuestions.state,
zipCode: newValue.addressQuestions.zipCode,
isVehicleProtected: newValue.isVehicleProtected,
});
},
deep: true,
},

View file

@ -122,10 +122,10 @@ import { errorMessages } from "@/constants/error-messages";
// DEFINE VALIDATION RULES
defineRule("mobile-location-required", (value) => {
if (
value.addressQuestions.streetAddress == "" ||
value.addressQuestions.city == "" ||
value.addressQuestions.state == "" ||
value.addressQuestions.zipCode == "" ||
value.addressQuestions.streetAddress == null ||
value.addressQuestions.city == null ||
value.addressQuestions.state == null ||
value.addressQuestions.zipCode == null ||
value.isVehicleProtected == null
) {
return errorMessages.MOBILE_LOCATION_REQUIRED;

View file

@ -325,9 +325,14 @@ export default {
await this.dispatchStoreAction(
storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
{
zipCode: this.serviceZipCode,
address: "",
address2: "",
city: "",
state: resultMap.zipCodeData.state,
zipCode: this.serviceZipCode,
zipCodeCtu: resultMap.zipCodeData.zipCodeCtu,
appointmentType: "",
isVehicleProtected: null,
},
false
);
@ -352,8 +357,8 @@ export default {
{
address: vehicleRegistrationInfo.address,
city: vehicleRegistrationInfo.city,
zipCode: vehicleRegistrationInfo.zipCode,
state: vehicleRegistrationInfo.state,
zipCode: vehicleRegistrationInfo.zipCode,
zipCodeCtu: zipCodeData.zipCodeCtu,
},
false
@ -362,9 +367,14 @@ export default {
await this.dispatchStoreAction(
storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
{
zipCode: this.serviceZipCode,
address: null,
address2: null,
city: null,
state: zipCodeData.state,
zipCode: this.serviceZipCode,
zipCodeCtu: zipCodeData.zipCodeCtu,
appointmentType: null,
isVehicleProtected: null,
},
false
);

View file

@ -253,9 +253,15 @@ export const mutations = {
state.order.serviceLocation.appointmentType = serviceLocationInfo.appointmentType;
state.order.serviceLocation.isVehicleProtected = serviceLocationInfo.isVehicleProtected;
if (serviceLocationInfo.provider) {
state.order.serviceLocation.provider = serviceLocationInfo.provider;
}
state.order.serviceLocation.provider = {
providerNumber: serviceLocationInfo.provider?.providerNumber,
address: {
streetAddress: serviceLocationInfo.provider?.address?.streetAddress,
city: serviceLocationInfo.provider?.address?.city,
state: serviceLocationInfo.provider?.address?.state,
zip: serviceLocationInfo.provider?.address?.zip,
},
};
},
updateSchedule(state, scheduleInfo) {
if (scheduleInfo) {
@ -362,7 +368,15 @@ export const mutations = {
state.order.serviceLocation.appointmentType = null;
},
resetServiceLocationProvider(state) {
state.order.serviceLocation.provider = null;
state.order.serviceLocation.provider = {
providerNumber: null,
address: {
streetAddress: null,
city: null,
state: null,
zip: null,
},
};
},
resetServiceLocationMobileAddress(state) {
state.order.serviceLocation.address = null;