Merge branch 'develop' into feature/CSR-1370

This commit is contained in:
Chloe Herd 2023-07-18 16:42:31 -04:00
commit f614e795a6
35 changed files with 1185 additions and 434 deletions

View file

@ -29,6 +29,10 @@ const errorMessages = {
DATE_REQUIRED: "Please select a date",
PHONE_REQUIRED: "Please enter your phone number",
PHONE_FORMAT: "Phone number must be 10 digits",
YEAR_REQUIRED: "Please select your vehicle year",
MAKE_REQUIRED: "Please select your vehicle make",
MODEL_REQUIRED: "Please select your vehicle model",
STYLE_REQUIRED: "Please select your vehicle style",
};
export { errorMessages };

View file

@ -74,7 +74,7 @@ https://safelite.atlassian.net/wiki/spaces/DC/pages/76644418/Button+Question+Com
</div>
<div class="row form-test-error">
<error-message
class="small"
class="small mt-1"
:name="formatString(groupName)"
v-if="!suppressError"></error-message>
</div>

View file

@ -6,7 +6,7 @@
v-for="month in months"
:key="`${month.monthLabel}-${month.yearNum?.toString()}`"
:id="`${month.monthLabel}-${month.yearNum?.toString()}`"
class="calendar-grid-container position-relative"
class="calendar-grid-container"
:class="[
hideSomeDaysForInitialView ? 'partial-month-initial-view' : '',
month.monthClass,
@ -56,15 +56,15 @@
:class="[!isLoading ? 'date-picker-hidden' : '']"
loaderColor="blue"
loaderPosition="center" />
<button
v-if="calendarViewDirection === 'future' && !disableViewMoreDatesButton"
type="button"
class="btn btn-link"
:disabled="isLoading"
@click="showAnotherMonth">
View more dates
</button>
</fieldset>
<button
v-if="calendarViewDirection === 'future' && !disableViewMoreDatesButton"
type="button"
class="btn btn-link"
:disabled="isLoading"
@click="showAnotherMonth">
View more dates
</button>
</div>
</template>
@ -74,6 +74,10 @@ import loader from "@/ux-components/loader/loader";
import store from "@/store";
import { TIMINGFUNC_MAP, BUFFER_OFFSET, MONTHS_OF_YEAR } from "./mixins/constants";
import { selectableDaysOptions, requiredParameter, forceTwoDigitString } from "./mixins/helpers";
import {
convertDateToDateString,
convertDateStringToDate,
} from "@/layouts/schedule/helpers/schedule-helper";
export default {
name: "datePicker",
@ -110,23 +114,14 @@ export default {
},
},
computed: {
today() {
if (this.todayOverrideDateString) {
return new Date(this.todayOverrideDateString + "T00:00:00");
}
return new Date();
},
todayMonthIndex() {
return this.today.getMonth() + 1;
},
todayYearNum() {
return this.today.getFullYear();
todayString() {
return this.todayOverrideDateString || convertDateToDateString(new Date());
},
todayDayIndex() {
return this.today.getDay();
return convertDateStringToDate(this.todayString).getDay();
},
todayDateNum() {
return this.today.getDate();
return convertDateStringToDate(this.todayString).getDate();
},
currentWeekStartDateNum() {
return this.todayDayIndex >= this.todayDateNum
@ -157,154 +152,187 @@ export default {
fireDateClickedEvent() {
this.$emit("date-clicked");
},
getWeekStartDate(date) {
getWeekStartDate(dateString) {
const date = convertDateStringToDate(dateString);
const dayOfWeek = date.getDay();
// Subtract the day of the week from date to get the date of Sunday
const sunday = new Date(date);
sunday.setDate(sunday.getDate() - dayOfWeek);
return sunday;
return convertDateToDateString(sunday);
},
getWeekEndDate(date) {
getWeekEndDate(dateString) {
const date = convertDateStringToDate(dateString);
const dayOfWeek = date.getDay();
const daysUntilSaturday = 6 - dayOfWeek; // Calculate the number of days until Saturday
// Clone the given date and add the remaining days until Saturday
const saturday = new Date(date);
saturday.setDate(date.getDate() + daysUntilSaturday);
return saturday;
return convertDateToDateString(saturday);
},
getNextWeekSunday(date) {
getNextWeekSunday(dateString) {
const date = convertDateStringToDate(dateString);
const dayOfWeek = date.getDay();
const daysUntilNextSunday = 7 - dayOfWeek; // Calculate the number of days until the next Sunday
// Clone the given date and add the remaining days until Sunday
const nextSunday = new Date(date);
nextSunday.setDate(date.getDate() + daysUntilNextSunday);
return nextSunday;
return convertDateToDateString(nextSunday);
},
getInitialViewWeeks(today, initialViewRowsToShow, preSelectedDateString) {
getMonthEnd(dateStr) {
// convert string to date, do date calc, then return a string back
const date = new Date(dateStr.split("-")[0], parseInt(dateStr.split("-")[1]), 0);
return convertDateToDateString(date);
},
getInitialViewWeeks(todayString, initialViewRowsToShow, preSelectedDateString) {
// TODO: this only is for future direction; need to create logic for past direction
const weeks = [];
let weekStartDate = this.getWeekStartDate(todayString);
let weekEndDate = this.getWeekEndDate(todayString);
if (preSelectedDateString) initialViewRowsToShow = 26;
if (preSelectedDateString) {
let preSelectedDateMonthEnd = this.getMonthEnd(preSelectedDateString);
let weekIncludesPreSelectedMonthEnd = false;
let i = 0;
while (!weekIncludesPreSelectedMonthEnd) {
if (i > 0) {
weekStartDate = this.getNextWeekSunday(weekEndDate);
weekEndDate = this.getWeekEndDate(weekStartDate);
let weekStartDate = this.getWeekStartDate(today);
let weekEndDate = this.getWeekEndDate(today);
for (let i = 0; i < initialViewRowsToShow; i++) {
if (i > 0) {
weekStartDate = this.getNextWeekSunday(weekEndDate);
weekEndDate = this.getWeekEndDate(weekStartDate);
}
weeks.push({
weekNum: i + 1,
weekStartDate: weekStartDate,
weekEndDate: weekEndDate,
});
if (
preSelectedDateString &&
new Date(preSelectedDateString + "T00:00:00") < weekEndDate
) {
break;
}
}
// are any of these weeks split between two months?
// NOTE: a week split between two months counts as 2 weeks
const hasSplitWeek = (week) => {
return week.weekStartDate.getMonth() !== week.weekEndDate.getMonth() ? true : false;
};
const splitWeekIndex = weeks.findIndex(hasSplitWeek);
if (!preSelectedDateString && splitWeekIndex > -1) {
// a preSelectedDateString precludes split week logic
const week1 = [];
const week2 = [];
let switchToWeek2 = false;
for (let j = 0; j < 7; j++) {
const newDate = new Date(weeks[splitWeekIndex].weekStartDate);
newDate.setDate(newDate.getDate() + j);
if (newDate.getDate() === 1) switchToWeek2 = true;
if (switchToWeek2) {
week2.push(newDate);
} else {
week1.push(newDate);
}
}
const week1EndDate = week1[week1.length - 1];
const week2StartDate = week2[0];
if (week1EndDate < today) {
// replace week 1 with week 2
weeks[splitWeekIndex].weekStartDate = week2StartDate;
} else {
const newWeek = {
weekNum: weeks[splitWeekIndex].weekNum,
weekStartDate: week2StartDate,
weekEndDate: weeks[splitWeekIndex].weekEndDate,
};
weeks[splitWeekIndex].weekEndDate = week1EndDate;
weeks.splice(splitWeekIndex + 1, 0, newWeek);
weeks.pop();
weeks.forEach((item, index) => {
if (index > splitWeekIndex) {
item.weekNum = item.weekNum + 1;
if (
(preSelectedDateMonthEnd > weekStartDate &&
preSelectedDateMonthEnd < weekEndDate) ||
preSelectedDateMonthEnd === weekStartDate ||
preSelectedDateMonthEnd === weekEndDate
) {
weekEndDate = preSelectedDateMonthEnd;
weekIncludesPreSelectedMonthEnd = true;
}
}
weeks.push({
weekNum: i + 1,
weekStartDate: weekStartDate,
weekEndDate: weekEndDate,
});
i++;
}
} else {
for (let i = 0; i < initialViewRowsToShow; i++) {
if (i > 0) {
weekStartDate = this.getNextWeekSunday(weekEndDate);
weekEndDate = this.getWeekEndDate(weekStartDate);
}
weeks.push({
weekNum: i + 1,
weekStartDate: weekStartDate,
weekEndDate: weekEndDate,
});
}
// If any of these weeks is split between two months, then make them 2 separate "weeks"
// (a week split between two months is considered 2 weeks per business requirements)
const hasSplitWeek = (week) => {
return week.weekStartDate.split("-")[1] !== week.weekEndDate.split("-")[1]
? true
: false;
};
const splitWeekIndex = weeks.findIndex(hasSplitWeek);
if (splitWeekIndex > -1) {
const week1 = [];
const week2 = [];
let switchToWeek2 = false;
for (let j = 0; j < 7; j++) {
const newDate = convertDateStringToDate(
weeks[splitWeekIndex].weekStartDate
);
newDate.setDate(newDate.getDate() + j);
if (newDate.getDate() === 1) switchToWeek2 = true;
if (switchToWeek2) {
week2.push(convertDateToDateString(newDate));
} else {
week1.push(convertDateToDateString(newDate));
}
}
const week1EndDate = week1[week1.length - 1];
const week2StartDate = week2[0];
if (week1EndDate < todayString) {
// replace week 1 with week 2
weeks[splitWeekIndex].weekStartDate = week2StartDate;
} else {
const newWeek = {
weekNum: weeks[splitWeekIndex].weekNum,
weekStartDate: week2StartDate,
weekEndDate: weeks[splitWeekIndex].weekEndDate,
};
weeks[splitWeekIndex].weekEndDate = week1EndDate;
weeks.splice(splitWeekIndex + 1, 0, newWeek);
weeks.pop();
weeks.forEach((item, index) => {
if (index > splitWeekIndex) {
item.weekNum = item.weekNum + 1;
}
});
}
}
}
return weeks;
},
async loadInitialData(config) {
let todayDate;
if (this.today) {
todayDate = this.today;
} else if (config.todayOverrideDateString) {
todayDate = new Date(config.todayOverrideDateString + "T00:00:00");
} else {
todayDate = new Date();
}
const todayMonthIndex = todayDate.getMonth() + 1;
const todayYearNum = todayDate.getFullYear();
// TODO - set up currentMonthStart if direction is PAST:
// let currentMonthStart = new Date(todayYearNum, todayMonthIndex - 1, 1);
const currentMonthEnd = new Date(todayYearNum, todayMonthIndex, 0);
/*
** NOTE: this _could_ be called by a parent before fully loaded, so data or computeds might not be available
*/
let todayDateString;
let calendarViewDirection = "none";
if (config.selectableDatesSetting === "past") calendarViewDirection = "past";
if (config.selectableDatesSetting === "custom") calendarViewDirection = "future";
const initialViewWeeks = this.getInitialViewWeeks(
todayDate,
config.initialViewRowsToShow,
config.preSelectedDate
);
const initialViewStartDate = todayDate;
const initialViewEndDate = initialViewWeeks[initialViewWeeks.length - 1].weekEndDate;
const firstSaturdayMonth = initialViewWeeks[0].weekEndDate.getMonth();
const lastSundayMonth =
initialViewWeeks[initialViewWeeks.length - 1].weekStartDate.getMonth();
let hideSomeDaysForInitialView = false;
let hideSecondMonth = false;
// TODO - NEED TO CREATE OPPOSITE LOGIC FOR "past" DIRECTION LIKE BELOW v v v
if (calendarViewDirection === "future" && !config.preSelectedDate) {
if (firstSaturdayMonth !== lastSundayMonth) {
hideSomeDaysForInitialView = true;
}
if (initialViewStartDate.getMonth() === lastSundayMonth) {
hideSecondMonth = true;
if (currentMonthEnd > initialViewEndDate) {
// should part of 1st month be hidden?
if (this.todayString) {
todayDateString = this.todayString;
} else if (config.todayOverrideDateString) {
todayDateString = config.todayOverrideDateString;
} else {
todayDateString = convertDateToDateString(new Date());
}
if (config.selectableDatesSetting === "past") calendarViewDirection = "past";
if (config.selectableDatesSetting === "custom") calendarViewDirection = "future";
const currentMonthEnd = this.getMonthEnd(todayDateString);
// TODO - set up currentMonthStart if direction is PAST:
// let currentMonthStart = new Date(todayYearNum, todayMonthIndex - 1, 1);
const initialViewWeeks = this.getInitialViewWeeks(
todayDateString,
config.initialViewRowsToShow,
config.preSelectedDate
);
const initialViewStartDate = todayDateString;
const initialViewEndDate = initialViewWeeks[initialViewWeeks.length - 1].weekEndDate;
if (!config.preSelectedDate) {
const firstSaturdayMonth = initialViewWeeks[0].weekEndDate.split("-")[1];
const lastSundayMonth =
initialViewWeeks[initialViewWeeks.length - 1].weekStartDate.split("-")[1];
if (calendarViewDirection === "future") {
if (firstSaturdayMonth !== lastSundayMonth) {
hideSomeDaysForInitialView = true;
}
if (initialViewStartDate.split("-")[1] === lastSundayMonth) {
hideSecondMonth = true;
if (currentMonthEnd > initialViewEndDate) {
// should part of 1st month be hidden?
hideSomeDaysForInitialView = true;
}
}
}
// TODO - NEED TO CREATE OPPOSITE LOGIC FOR "past" DIRECTION LIKE THE ABOVE ^ ^ ^
}
const loadInitialDataPromise = new Promise((resolve, reject) => {
const response = config.customSelectableDatesCallback(
initialViewStartDate.toISOString().split("T")[0],
initialViewEndDate.toISOString().split("T")[0],
initialViewStartDate,
initialViewEndDate,
store.getters.order.serviceLocation.appointmentType,
store.getters.order.serviceLocation.provider.providerNumber
);
@ -313,7 +341,7 @@ export default {
return loadInitialDataPromise.then((response) => {
const initialData = {
todayDate: todayDate,
todayDate: todayDateString,
initialViewStartDate: initialViewStartDate,
initialViewEndDate: initialViewEndDate,
calendarViewDirection: calendarViewDirection,
@ -329,7 +357,7 @@ export default {
this.hideSomeDaysForInitialView = config.hideSomeDaysForInitialView;
const hideSecondMonth = config.hideSecondMonth;
const direction = config.calendarViewDirection;
const monthsAfterToLoadOffset = 12;
const monthsAfterToLoadOffset = 6;
const monthsBeforeToLoadOffset = 36;
config.initialShopTimeSlotsResponse.days.forEach((selectableDate) => {
this.selectableDatesData.push(selectableDate);
@ -371,6 +399,13 @@ export default {
const monthToShow = this.months.find((month) =>
month.monthClass.includes("month-preselected")
);
if (
monthToShow.monthClass.includes("month-preselected") &&
monthToShow.monthClass.includes("last-available-month")
) {
// disable View More dates button if the preSelectedDate in the last available month
this.disableViewMoreDatesButton = true;
}
this.scrollToElement(monthToShow.monthString);
});
}
@ -390,32 +425,37 @@ export default {
this.hideSomeDaysForInitialView (string)
*/
let monthIndex = this.todayMonthIndex + offset; // mutable
let yearNum = this.todayYearNum; // mutable
let monthNum = convertDateStringToDate(this.todayString).getMonth() + offset + 1;
let yearNum = convertDateStringToDate(this.todayString).getFullYear();
const calendarViewDirection = options.calendarViewDirection;
const initialViewStartDate = options.initialViewStartDate;
const initialViewEndDate = options.initialViewEndDate;
const hideSecondMonth = options.hideSecondMonth;
const preSelectedDateObj = options.preSelectedDate
? new Date(options.preSelectedDate + "T00:00:00")
: null;
const dates = [];
let monthClass = "";
let isMonthThatHidesSomeDaysForInitialView;
if (calendarViewDirection === "future" && offset > 0) {
while (monthIndex > 12) {
monthIndex = monthIndex - 12;
while (monthNum > 12) {
monthNum = monthNum - 12;
yearNum++;
}
} else if (calendarViewDirection === "past" && offset < 0) {
while (monthIndex < 1) {
monthIndex = 12 + monthIndex;
while (monthNum < 1) {
monthNum = 12 + monthNum;
yearNum--;
}
}
const initialViewEndDate = options.initialViewEndDate;
// const initialViewStartDate = options.initialViewStartDate; // TODO: to be used for past calendarViewDirection
const hideSecondMonth = options.hideSecondMonth;
const preSelectedDateObj = options.preSelectedDate
? convertDateStringToDate(options.preSelectedDate)
: null;
const dates = [];
const monthEndDate = new Date(yearNum, monthNum, 0);
const monthStartDateNum =
offset === 0 && calendarViewDirection === "future"
? this.currentWeekStartDateNum
: 1;
const monthStartDate = new Date(yearNum, monthNum - 1, monthStartDateNum);
const startDateDayIndex = monthStartDate.getDay();
const monthEndDate = new Date(yearNum, monthIndex, 0);
let monthClass = "";
let isMonthThatHidesSomeDaysForInitialView;
let monthEndDateNum = monthEndDate.getDate();
if (
@ -426,16 +466,7 @@ export default {
monthEndDateNum = this.currentWeekEndDateNum;
}
const monthStartDateNum =
offset === 0 && calendarViewDirection === "future"
? this.currentWeekStartDateNum
: 1;
const monthStartDate = new Date(yearNum, monthIndex - 1, monthStartDateNum);
const startDateDayIndex = monthStartDate.getDay();
const endDateDayIndex = monthEndDate.getDay();
if (preSelectedDateObj) {
if (options.preSelectedDate) {
if (
monthStartDate.getFullYear() === preSelectedDateObj.getFullYear() &&
monthStartDate.getMonth() === preSelectedDateObj.getMonth()
@ -472,25 +503,28 @@ export default {
const dateString =
yearNum.toString() +
"-" +
forceTwoDigitString(monthIndex) +
("0" + monthNum).slice(-2) +
"-" +
forceTwoDigitString(i);
("0" + i).slice(-2);
if (offset === 0 && i === this.todayDateNum) {
dayClasses += "current-day";
dayClasses += " current-day";
}
if (offset === 0 && i < this.todayDateNum && calendarViewDirection === "future") {
dayClasses += "unavailable-day";
dayClasses += " unavailable-day";
}
if (offset === 0 && i > this.todayDateNum && calendarViewDirection === "past") {
dayClasses += "unavailable-day";
dayClasses += " unavailable-day";
}
if (convertDateStringToDate(dateString).getDay() === 0) {
dayClasses += " sunday";
}
if (
this.hideSomeDaysForInitialView &&
initialViewEndDate.getMonth() + 1 === monthIndex &&
initialViewEndDate.getDate() < i
convertDateStringToDate(initialViewEndDate).getMonth() + 1 === monthNum &&
convertDateStringToDate(initialViewEndDate).getDate() < i
) {
dayClasses += "day-hidden";
dayClasses += " day-hidden";
isMonthThatHidesSomeDaysForInitialView = true;
}
const dateObject = {
@ -506,9 +540,9 @@ export default {
}
const monthToAdd = {
monthLabel: MONTHS_OF_YEAR[monthIndex - 1],
monthIndex: monthIndex,
monthString: MONTHS_OF_YEAR[monthIndex - 1] + "-" + yearNum?.toString(),
monthLabel: MONTHS_OF_YEAR[monthNum - 1],
monthIndex: monthNum,
monthString: MONTHS_OF_YEAR[monthNum - 1] + "-" + yearNum?.toString(),
yearNum: yearNum,
dates: dates,
startDateDayIndex: startDateDayIndex,
@ -626,28 +660,28 @@ export default {
max-height: 0;
}
.date-picker {
overflow: hidden;
position: relative;
height: 100%;
flex-grow: 1;
display: flex;
flex-direction: column;
fieldset {
overflow-y: auto;
height: 88%;
flex-grow: 1;
position: relative;
}
.loader {
position: absolute;
height: 2rem;
width: 2rem;
width: calc(100% - 1.5rem);
&::after {
width: 100%;
height: 100%;
width: 1.5rem;
height: 1.5rem;
}
}
.calendar-grid-container {
margin: 0 auto 2rem auto;
max-width: 414px;
position: relative;
transition: height ease 2s, opacity ease 2s;
display: grid;
grid-template-columns: repeat(7, 1fr);
@ -659,6 +693,8 @@ export default {
.grid-item {
text-align: center;
margin: 10px 3px;
font-size: 0.875rem;
line-height: 1.5;
&.first-day-,
&.first-day-0 {
@ -687,7 +723,7 @@ export default {
.separator-line {
grid-area: 2/1/2/8;
border-top: 1px solid $gray-500;
margin: 0.75rem 0;
margin: 0.5rem 0;
}
.month-year {
@ -717,7 +753,7 @@ export default {
justify-content: center;
align-items: center;
outline: none;
height: 1.35rem;
height: 1.5rem;
opacity: 1;
transition: height ease 250ms, opacity ease 250ms;
@ -735,12 +771,6 @@ export default {
box-shadow: 0 0 0 3px #fff, 0 0 0 5.5px #1574a1;
background-color: $blue;
color: $white;
&.past-day {
box-shadow: none;
background-color: transparent;
color: $gray-500;
font-weight: normal;
}
&.current-day {
&:after {
background-color: $white;
@ -754,7 +784,6 @@ export default {
&:checked + label {
color: $white;
background: $blue;
box-shadow: 0 0 0 3px $white, 0 0 0 5.5px $blue;
&:after {
background-color: $white;
}
@ -779,10 +808,9 @@ export default {
display: flex;
justify-content: center;
align-items: center;
width: 36px;
height: 36px;
min-width: 36px;
border-radius: 50%;
min-width: 2.5rem;
width: 2.5rem;
height: 2.5rem;
span {
&.small {
@ -821,26 +849,43 @@ export default {
color: $blue;
background-color: $blue-100;
border: 1px solid $blue;
min-width: 2.5rem;
width: 2.5rem;
border-radius: 50%;
}
}
&.past-day,
&.future-day,
&.unavailable-day {
label {
color: $gray-500;
background-color: transparent;
border: 1px solid transparent;
background-color: $gray-100;
border: none;
pointer-events: none;
}
}
&.unavailable-day:not(&.sunday) {
label {
&::before {
content: "";
display: inline-block;
position: absolute;
left: -1rem;
width: 1rem;
height: 2.5rem;
background: $gray-100;
}
}
}
&.current-day {
label {
font-weight: 500;
color: $black;
&:after {
content: "";
width: 0.25rem;
height: 0.25rem;
border-radius: 50%;
background-color: $blue;
background-color: $black;
position: absolute;
top: 28px;
}
@ -864,15 +909,12 @@ export default {
opacity: 0;
max-height: 0;
margin-bottom: 0;
overflow: hidden;
}
&.last-available-month:not(&.month-hidden) {
margin-bottom: 14rem;
}
}
#bottom-spacer {
height: 20rem;
background: lightblue;
}
.btn-link {
font-weight: 500;

View file

@ -7,9 +7,4 @@ const requiredParameter = () => {
throw new Error("parameter is required");
};
const forceTwoDigitString = (monthNum) => {
const newString = monthNum.toString();
return newString.length === 1 ? "0" + newString : newString;
};
export { selectableDaysOptions, requiredParameter, forceTwoDigitString };
export { selectableDaysOptions, requiredParameter };

View file

@ -44,6 +44,7 @@ export default {
isDisabled: Boolean,
isRequired: Boolean,
validationRules: String,
placeHolderText: String,
cmsWidgetName: String,
hasError: Boolean,
},

View file

@ -139,6 +139,13 @@ export default {
color: rgb(0, 0, 0);
}
}
.modal-footer {
position: sticky;
width: 100%;
bottom: 0;
z-index: 5;
background-color: $gray-100;
}
&.modal-component {
.modal-dialog {
max-width: 576px;

View file

@ -65,9 +65,9 @@
</template>
</template>
</div>
<div v-show="errorMessage" class="row my-1 form-test-error">
<div v-show="errorMessage" class="row form-test-error">
<span
class="d-inline-flex small mt-0"
class="d-inline-flex small my-1"
role="alert"
:class="[centerErrorMessage ? 'center-error-message' : '']"
>{{ errorMessage }}</span

View file

@ -13,6 +13,7 @@
:isDisabled="isForwardActionDisabled"
@click-event="buttonClick"
data-bs-target="#footerModal"
data-test-id="funnel-footer-main-button"
data-bs-dismiss="modal" />
</div>
<div v-if="!isBackButtonHidden" class="col-auto link-col py-1 text-break">

View file

@ -39,9 +39,11 @@ export default {
}
const pageName = analyticsMixIn.methods.getPageName();
const nextPageName = router.lastNavigationPage || pageName;
const endpointWithoutParams =
analyticsMixIn.methods.removeParamsFromEndpoint(endpoint);
analyticsMixIn.methods.pushEventToGA(
GaCategories.API_RESPONSE,
`${nextPageName}_${endpoint}`,
`${nextPageName}_${endpointWithoutParams}`,
`${GaLabels.SUCCESS}${additionalEventData}`,
true
);

View file

@ -296,7 +296,7 @@ export function doesCopyContainTextLink(copy) {
* @returns array of strings
*/
export function splitCopyOnCMSPlaceHolder(copy) {
return copy.split(/{(.*?)}/g);
return copy.split(/{(.*?)}/g).filter((str) => str.length > 0);
}
/**

View file

@ -27,7 +27,7 @@
v-model="customerModel.emailAddress"
ref="emailAddress"
customInputId="emailAddress"
validationRules="email-address-format" />
validationRules="email-address-required|email-address-format" />
</div>
</div>
<div class="row mb-0">
@ -49,6 +49,7 @@ import textBlock from "@/digital-components/text-block/text-block";
// DEFINE VALIDATION RULES
defineRule("first-name-required", required(errorMessages.FIRST_NAME_REQUIRED));
defineRule("last-name-required", required(errorMessages.LAST_NAME_REQUIRED));
defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
defineRule(
"email-address-format",
regex(

View file

@ -151,6 +151,7 @@ export default {
if (
store.getters.order.vehicle.carId &&
store.getters.order.serviceLocation.zipCode &&
store.getters.order.customer.emailAddress &&
store.getters.pageData(fmgPageValues.ADDRESS_VEHICLES)
) {
return true;

View file

@ -44,8 +44,9 @@
cmsWidgetName="EmailAddressQuestionWidget"
v-model="emailAddress"
inputId="emailAddress"
isRequired
disableAutoFill
validationRules="email-address-format" />
validationRules="email-address-required|email-address-format" />
</div>
</div>
<div class="row mb-2">
@ -117,6 +118,7 @@ import { queryStrings } from "@/constants/query-strings";
// Define Validation Rules
defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED));
defineRule("zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT));
defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
defineRule(
"email-address-format",
regex(

View file

@ -33,7 +33,7 @@
cmsWidgetName="EmailAddressQuestionWidget"
v-model="email"
customInputId="email"
validationRules="email-address-format" />
validationRules="email-address-required|email-address-format" />
</div>
</div>
<div class="row mb-2">
@ -115,6 +115,7 @@ import vinPagesMixin from "@/mixins/vin-pages-mixin";
defineRule("license-plate-required", required(errorMessages.LICENSE_PLATE_REQUIRED));
defineRule("registration-zip-required", required(errorMessages.REGISTRATION_ZIP_REQUIRED));
defineRule("zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT));
defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
defineRule(
"email-address-format",
regex(

View file

@ -18,3 +18,30 @@ export function calcDaysBetweenDates(dateString1, dateString2) {
const timeDifference = Math.abs(date2 - date1); // Calculate the time difference in milliseconds
return Math.ceil(timeDifference / (1000 * 60 * 60 * 24)); // Convert milliseconds to days
}
export function convertDateToDateString(date) {
// returns YYYY-MM-DD format
if (date instanceof Date !== true) return;
return (
date.getFullYear() +
"-" +
("0" + (date.getMonth() + 1)).slice(-2) +
"-" +
("0" + date.getDate()).slice(-2)
);
}
export function convertDateStringToDate(dateString) {
// dateString must be YYYY-MM-DD format
if (typeof dateString !== "string") return;
const dateParts = dateString.split("-");
return new Date(dateParts[0], parseInt(dateParts[1]) - 1, dateParts[2]);
}
export function sumDateString(dateString, daysToAdd) {
// dateString must be YYYY-MM-DD format
if (typeof dateString !== "string") return;
const date = convertDateStringToDate(dateString);
date.setDate(date.getDate() + daysToAdd);
return convertDateToDateString(date);
}

View file

@ -8,7 +8,7 @@
<textBlock
cmsWidgetName="ChangeShopLink"
justifyText="center"
class="mb-3"
class="mb-3 text-link-small"
marginTopSizeOverride="1" />
</template>
<location-alerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" />
@ -16,6 +16,7 @@
selectableDatesSetting="custom"
ref="datePicker"
v-model="selectedDate"
class="text-link-small"
:customSelectableDatesCallback="getAvailableDatesMethod"
@date-clicked="openInshopTimeSlotsModal" />
<time-slot-modal-question
@ -63,7 +64,11 @@ import { storeActions } from "@/constants/store-actions";
import { settleAllPromises } from "@/helpers/layout-helper";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { splitCopyOnCMSPlaceHolder } from "@/helpers/cms-content-helper";
import { calcDaysBetweenDates } from "@/layouts/schedule/helpers/schedule-helper";
import {
calcDaysBetweenDates,
convertDateStringToDate,
sumDateString,
} from "@/layouts/schedule/helpers/schedule-helper";
import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants";
import { errorMessages } from "@/constants/error-messages";
import { required } from "@/helpers/validation-rules";
@ -81,49 +86,41 @@ const getAvailableDates = async (
appointmentType,
providerNumber
) => {
var today = new Date();
var currentTime = today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds();
const apiEndDateLimit = new Date(startDateString + "T" + currentTime);
const endDate = new Date(endDateString + "T" + currentTime);
apiEndDateLimit.setDate(apiEndDateLimit.getDate() + TIME_SLOTS_CALL_DAYS_LIMIT);
// how many days are between startDate and endDate?
const apiEndDateLimit = sumDateString(startDateString, TIME_SLOTS_CALL_DAYS_LIMIT);
const difference = calcDaysBetweenDates(startDateString, endDateString);
const apiCallsCount = Math.ceil(difference / TIME_SLOTS_CALL_DAYS_LIMIT);
const storeActionConfigs = [];
const timeSlotsData = {};
let apiStartDate = new Date(startDateString + "T" + currentTime);
let apiEndDate = apiEndDateLimit;
timeSlotsData.days = [];
let apiStartDate = startDateString;
let apiEndDate = apiEndDateLimit;
for (let i = 1; i <= apiCallsCount; i++) {
let storeActionConfig;
if (i > 1) {
apiStartDate = new Date(apiEndDate);
apiStartDate.setDate(apiStartDate.getDate() + 1);
apiEndDate = new Date(apiStartDate);
apiEndDate.setDate(apiEndDate.getDate() + TIME_SLOTS_CALL_DAYS_LIMIT);
}
if (i === apiCallsCount) {
apiEndDate = new Date(endDateString + "T" + currentTime);
apiStartDate = sumDateString(apiEndDate, 1);
apiEndDate = sumDateString(apiStartDate, TIME_SLOTS_CALL_DAYS_LIMIT);
if (i === apiCallsCount) {
apiEndDate = endDateString;
}
}
if (appointmentType === AppointmentTypeStrings.MOBILE) {
storeActionConfig = {
storeAction: storeActions.GET_MOBILE_TIME_SLOTS,
payload: {
startDate: apiStartDate.toISOString().split("T")[0],
endDate: apiEndDate.toISOString().split("T")[0],
startDate: apiStartDate,
endDate: apiEndDate,
},
};
} else {
storeActionConfig = {
storeAction: storeActions.GET_SHOP_TIME_SLOTS,
payload: {
startDate: apiStartDate.toISOString().split("T")[0],
endDate: apiEndDate.toISOString().split("T")[0],
startDate: apiStartDate,
endDate: apiEndDate,
shopAppointmentType: appointmentType,
providerNumber: providerNumber,
},
@ -384,12 +381,11 @@ export default {
);
}
}
this.$refs.funnelFooter.updateButtonText(funnelFooterButtonText);
},
convertSelectedDateToShortMonthAndDay(selectedDate) {
// This conversion ensures we don't get get GMT induced date changes
const dateObject = new Date(`${selectedDate}T00:00:00`);
const dateObject = convertDateStringToDate(selectedDate);
// Ex: April 25
return dateObject.toLocaleDateString("en-us", { month: "short", day: "numeric" });
},
@ -496,3 +492,18 @@ export default {
},
};
</script>
<style lang="scss">
.text-link-small {
a,
.btn-link {
font-size: 0.875rem;
line-height: 1.5;
}
}
.funnel-sub-header {
h5.dark-header {
margin-bottom: 0.25rem;
}
}
</style>

View file

@ -41,6 +41,7 @@
import modal from "@/digital-components/modal/modal";
import textBlock from "@/digital-components/text-block/text-block";
import buttonQuestion from "@/digital-components/button-question/button-question";
import { convertDateStringToDate } from "@/layouts/schedule/helpers/schedule-helper";
import timeSlotModalListButton from "./time-slot-modal-list-button/time-slot-modal-list-button";
// TODO: Move this somewhere more global
@ -55,6 +56,13 @@ import {
PREMIUM_FEE_PART_TYPE,
RouteCodeFlags,
} from "@/constants/schedule-constants";
const cmsWidgetFieldMappings = {
MODAL_CLOSE_BUTTON: "FooterText",
SUPPLEMENTAL_INFORMATION: "BodyText",
TIME_SLOT_BUTTON: "HeaderText",
DISCLAIMER: "FooterText",
DURATION: "SubheaderText",
};
// Validation for the modal button
defineRule("time-slot-required", required(errorMessages.OPTION_REQUIRED));
@ -124,32 +132,64 @@ export default {
);
}
}
return this.getCmsContent(appointmentTypeCmsWidgetName, "BodyText");
return this.getCmsContent(
appointmentTypeCmsWidgetName,
cmsWidgetFieldMappings.SUPPLEMENTAL_INFORMATION
);
},
footerCloseButtonText() {
return this.getCmsContent(this.cmsWidgetName, "FooterText");
return this.getCmsContent(
this.cmsWidgetName,
cmsWidgetFieldMappings.MODAL_CLOSE_BUTTON
);
},
premiumAppointmentButtonText() {
return this.getCmsContent(this.mobilePremiumCmsWidgetName, "HeaderText");
return this.getCmsContent(
this.mobilePremiumCmsWidgetName,
cmsWidgetFieldMappings.TIME_SLOT_BUTTON
);
},
dropoffButtonText() {
return this.getCmsContent(this.dropoffCmsWidgetName, "HeaderText");
return this.getCmsContent(
this.dropoffCmsWidgetName,
cmsWidgetFieldMappings.TIME_SLOT_BUTTON
);
},
sameDayDropoffButtonText() {
return this.getCmsContent(
this.sameDayDropOffCmsWidgetName,
cmsWidgetFieldMappings.TIME_SLOT_BUTTON
);
},
overnightDropoffButtonText() {
return this.getCmsContent(this.overnightDropOffCmsWidgetName, "HeaderText");
return this.getCmsContent(
this.overnightDropOffCmsWidgetName,
cmsWidgetFieldMappings.TIME_SLOT_BUTTON
);
},
dropoffDisclaimerText() {
return this.getCmsContent(this.dropoffCmsWidgetName, "FooterText");
return this.getCmsContent(this.dropoffCmsWidgetName, cmsWidgetFieldMappings.DISCLAIMER);
},
sameDayDropOffDisclaimerText() {
return this.getCmsContent(
this.sameDayDropOffCmsWidgetName,
cmsWidgetFieldMappings.DISCLAIMER
);
},
overnightDropOffDisclaimerText() {
return this.getCmsContent(this.overnightDropOffCmsWidgetName, "FooterText");
return this.getCmsContent(
this.overnightDropOffCmsWidgetName,
cmsWidgetFieldMappings.DISCLAIMER
);
},
disclaimerTextBlockCopy() {
if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) {
if (this.isSameDay) {
return null;
} else if (this.selectedTimeSlotId?.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
return this.dropoffDisclaimerText;
if (this.selectedTimeSlotId?.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
if (this.isSameDay) {
return this.sameDayDropOffDisclaimerText;
} else {
return this.dropoffDisclaimerText;
}
} else if (this.selectedTimeSlotId?.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
return this.overnightDropOffDisclaimerText;
} else {
@ -160,15 +200,24 @@ export default {
}
},
dropOffDurationText() {
return this.getCmsContent(this.dropoffCmsWidgetName, "SubheaderText");
return this.getCmsContent(this.dropoffCmsWidgetName, cmsWidgetFieldMappings.DURATION);
},
sameDayDropoffDurationText() {
return this.getCmsContent(
this.sameDayDropOffCmsWidgetName,
cmsWidgetFieldMappings.DURATION
);
},
overnightDropoffDurationText() {
return this.getCmsContent(this.overnightDropOffCmsWidgetName, "SubheaderText");
return this.getCmsContent(
this.overnightDropOffCmsWidgetName,
cmsWidgetFieldMappings.DURATION
);
},
inshopDurationText() {
const inshopDurationTextWithoutTime = this.getCmsContent(
this.cmsWidgetName,
"SubheaderText"
cmsWidgetFieldMappings.DURATION
);
const inshopDurationTime = this.getDisplayTextForDurationLength(
this.estimatedServiceMinutesMinimum,
@ -185,15 +234,16 @@ export default {
if (this.selectedTimeSlotId?.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
return this.overnightDropoffDurationText;
} else if (this.selectedTimeSlotId?.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
return this.dropOffDurationText;
if (this.isSameDay) {
return this.sameDayDropoffDurationText;
} else {
return this.dropOffDurationText;
}
} else {
return null;
}
}
},
shouldShowDropoffDisclaimerText() {
return this.appointmentType === AppointmentTypeStrings.DROP_OFF && !this.isSameDay;
},
isSameDay() {
if (!this.dateAndTimeSlotData) {
return false;
@ -202,14 +252,13 @@ export default {
const todaysDate = new Date().toISOString().split("T")[0];
return selectedDate === todaysDate;
},
dateSelectedReadableDate() {
if (!this.dateAndTimeSlotData) {
return null;
}
// This conversion ensures we don't get get GMT induced date changes
const dateObject = new Date(`${this.dateAndTimeSlotData.date}T00:00:00`);
const dateObject = convertDateStringToDate(this.dateAndTimeSlotData.date);
// Ex: Tuesday, April 22
return dateObject.toLocaleDateString("en-us", {
weekday: "long",
@ -303,9 +352,14 @@ export default {
},
getAvailableTimeSlotsForDropOff(timeSlotsForSelectedDate) {
const availableTimeSlots = timeSlotsForSelectedDate.map((timeSlot) => {
const buttonLabelValue = timeSlot.id.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)
? this.overnightDropoffButtonText
: this.dropoffButtonText;
let buttonLabelValue;
if (timeSlot.id.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
buttonLabelValue = this.overnightDropoffButtonText;
} else if (this.isSameDay) {
buttonLabelValue = this.sameDayDropoffButtonText;
} else {
buttonLabelValue = this.dropoffButtonText;
}
return {
value: timeSlot.id,
buttonLabel: buttonLabelValue,

View file

@ -1,6 +1,13 @@
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import appointmentTypeQuestion from "./appointment-type-question";
import store from "@/store";
store.getters = {
damage: {
isRepair: false,
},
};
const mockCmsContent = {
QuestionText: "Choose a service option:",
@ -42,6 +49,15 @@ const mockMixin = {
},
};
afterEach(() => {
// reset store after each test
store.getters = {
damage: {
isRepair: false,
},
};
});
describe("appointment-type-question.vue", () => {
it("Should display all options if both in-shop and mobile are available", async () => {
// Arrange/Act
@ -141,6 +157,45 @@ describe("appointment-type-question.vue", () => {
},
]);
});
it("Should not display Drop off answer when it is repair order", async () => {
// Arrange/Act
store.getters = {
damage: {
isRepair: true,
},
};
const { wrapper } = setupMocks({
mixins: [mockMixin],
props: {
cmsWidgetName: cmsWidgetName,
isServiceableInshop: true,
isServiceableMobile: true,
},
mountOptions: {
attachTo: document.body,
},
});
// Assert
expect(wrapper.vm.answersToDisplay).toEqual([
{
AnswerImageUrl: "",
Name: "Mobile",
SubText: "",
SubWidgetName: "",
Text: "Mobile",
},
{
AnswerImageUrl: "",
Name: "Inshop",
SubText: "",
SubWidgetName: "",
Text: "In-shop",
},
]);
});
it("Should display no answers if neither in-shop nor mobile service are available", async () => {
// Arrange/Act
@ -169,6 +224,8 @@ function setupMocks({ mountOptions, mixins, props, isShallowMount = true }) {
if (props) resultingMountOptions.propsData = props;
resultingMountOptions.global.mocks["$store"] = store;
const wrapper = isShallowMount
? shallowMount(appointmentTypeQuestion, resultingMountOptions)
: mount(appointmentTypeQuestion, resultingMountOptions);

View file

@ -18,6 +18,7 @@
<script>
import buttonQuestion from "@/digital-components/button-question/button-question";
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
export default {
name: "appointment-type-question",
@ -40,20 +41,19 @@ export default {
return this.getCmsContent(this.cmsWidgetName, "Answers");
},
answersToDisplay() {
let filteredAnswers;
if (this.isServiceableMobile && this.isServiceableInshop) {
filteredAnswers = this.answersFromCms;
} else if (this.isServiceableMobile) {
filteredAnswers = this.answersFromCms.filter((answer) => answer.Name == "Mobile");
} else if (this.isServiceableInshop) {
filteredAnswers = this.answersFromCms.filter(
(answer) => answer.Name == "Inshop" || answer.Name == "Dropoff"
);
} else {
filteredAnswers = [];
}
return filteredAnswers;
const shouldShowMobile = this.isServiceableMobile;
const shouldShowInshop = this.isServiceableInshop;
const shouldShowDropoff =
this.isServiceableInshop && !this.$store.getters.damage.isRepair;
return this.answersFromCms
? this.answersFromCms.filter((answer) => {
return (
(answer.Name == AppointmentTypeStrings.IN_SHOP && shouldShowInshop) ||
(answer.Name == AppointmentTypeStrings.MOBILE && shouldShowMobile) ||
(answer.Name == AppointmentTypeStrings.DROP_OFF && shouldShowDropoff)
);
})
: [];
},
selectedValues: {
get: function () {

View file

@ -62,27 +62,10 @@ export async function getAvailabilityRating(
false
);
// Rate the availability for the shop
let numberOfAppointmentsPerDay = [];
for (let i = 0; i < shopTimeSlots.data.days.length; i++) {
numberOfAppointmentsPerDay.push(shopTimeSlots.data.days[i].timeSlots.length);
}
const dateRange = 7;
const minimumNumberOfAppointmentsPerDay = 1;
const numberOfDaysToEvaluate = 2;
let daysWithMinimalAppointmentsCount = 0;
for (let i = 0; i < dateRange; i++) {
if (numberOfAppointmentsPerDay[i] >= minimumNumberOfAppointmentsPerDay) {
daysWithMinimalAppointmentsCount++;
if (daysWithMinimalAppointmentsCount >= numberOfDaysToEvaluate) {
break;
}
}
}
const isGoodAvailability = daysWithMinimalAppointmentsCount >= numberOfDaysToEvaluate;
const isGoodAvailability =
shopTimeSlots.data.days.filter((x) => x.timeSlots.length > 0).length >=
numberOfDaysToEvaluate;
const shopStatus = isGoodAvailability ? "high" : "low";

View file

@ -93,94 +93,54 @@ const mockStoreActionGetServiceabilityDetails = storeActions.GET_SERVICEABILITY_
const mockStoreActionGetShopTimeSlots = storeActions.GET_SHOP_TIME_SLOTS;
const mockGetShopTimeSlotsGoodAvailability = {
estimatedServiceMinutesMinimum: 0,
estimatedServiceMinutesMaximimum: 0,
days: [
{
date: "string",
timeSlots: [
{
id: "string",
startTime: "",
endTime: "",
offerPremium: true,
},
],
},
{
date: "string",
timeSlots: [],
},
{
date: "string",
timeSlots: [],
},
{
date: "string",
timeSlots: [],
},
{
date: "string",
timeSlots: [],
},
{
date: "string",
timeSlots: [],
},
{
date: "string",
timeSlots: [
{
id: "string",
startTime: "",
endTime: "",
offerPremium: true,
},
],
},
],
data: {
estimatedServiceMinutesMinimum: 0,
estimatedServiceMinutesMaximimum: 0,
days: [
{
date: "string",
timeSlots: [
{
id: "string",
startTime: "",
endTime: "",
offerPremium: true,
},
],
},
{
date: "string",
timeSlots: [
{
id: "string",
startTime: "",
endTime: "",
offerPremium: true,
},
],
},
],
},
};
const mockGetShopTimeSlotsLowAvailability = {
estimatedServiceMinutesMinimum: 0,
estimatedServiceMinutesMaximimum: 0,
days: [
{
date: "string",
timeSlots: [],
},
{
date: "string",
timeSlots: [],
},
{
date: "string",
timeSlots: [],
},
{
date: "string",
timeSlots: [],
},
{
date: "string",
timeSlots: [],
},
{
date: "string",
timeSlots: [],
},
{
date: "string",
timeSlots: [
{
id: "string",
startTime: "",
endTime: "",
offerPremium: true,
},
],
},
],
data: {
estimatedServiceMinutesMinimum: 0,
estimatedServiceMinutesMaximimum: 0,
days: [
{
date: "string",
timeSlots: [
{
id: "string",
startTime: "",
endTime: "",
offerPremium: true,
},
],
},
],
},
};
jest.mock("@/mixins/base-mixin.js", () => ({
@ -311,23 +271,36 @@ describe("service-location-helper.js", () => {
});
describe("getAvailabilityRating", () => {
// it("Should return a 'Good' rating", async () => {
// // Arrange
// const providerNumber = "0000001";
// const expected = "Good";
// // Act
// const result = await getAvailabilityRating(providerNumber);
// // Assert
// expect(result).toEqual(expected);
// });
// it("Should return a 'Low' rating", async () => {
// // Arrange
// const providerNumber = "0000000";
// const expected = "Low";
// // Act
// const result = await getAvailabilityRating(providerNumber);
// // Assert
// expect(result).toEqual(expected);
// });
it("Should return a 'high' rating", async () => {
// Arrange
const providerNumber = "0000001";
const expected = "high";
// Act
const result = await getAvailabilityRating(
"2023-06-30",
"2023-07-06",
"Inshop",
providerNumber
);
// Assert
expect(result).toEqual(expected);
});
it("Should return a 'low' rating", async () => {
// Arrange
const providerNumber = "0000000";
const expected = "low";
// Act
const result = await getAvailabilityRating(
"2023-06-30",
"2023-07-06",
"Inshop",
providerNumber
);
// Assert
expect(result).toEqual(expected);
});
});
});

View file

@ -25,7 +25,10 @@
<span v-if="!isLoaderDisplayed" class="m-0 button-auxillary-copy">{{
badgeText
}}</span>
<loader v-if="isLoaderDisplayed" :class="['left', 'no-block']" />
<loader
v-if="isLoaderDisplayed"
:loaderPosition="left"
:allowPageInteraction="true" />
</div>
</div>
<div class="row-two">

View file

@ -108,7 +108,7 @@ export default {
additionalButtonData() {
const startDate = new Date();
const endDate = new Date();
endDate.setDate(startDate.getDate() + 7);
endDate.setDate(startDate.getDate() + 6);
const formattedStartDate = startDate.toISOString().split("T")[0];
const formattedEndDate = endDate.toISOString().split("T")[0];

View file

@ -0,0 +1,56 @@
import vehicleQuestion from "@/layouts/vehicle/vehicle-question/vehicle-question";
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store";
jest.mock(
"@/store",
() => {
return {};
},
{ virtual: true }
);
describe("vehicle-question.vue", () => {
test("Selected value is emitted upon selection.", async () => {
//Arrange
const { wrapper } = setupMocks({ modelValueProp: "value" });
const valueToSelect = "newvalue";
//Act
wrapper.setValue({ selectedValue: valueToSelect });
await wrapper.vm.$nextTick();
//Assert
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([{ selectedValue: "newvalue" }]);
});
});
function setupMocks({ modelValueProp = "", dataFromStoreApi = [] }) {
//Mock store
store.dispatch = jest.fn(() => dataFromStoreApi);
store.getters = { vehicle: { year: 2019, make: "honda", model: "civic", style: "4 Door" } };
const mountOptions = getMountOptions({
store: {
dispatch: store.dispatch,
getters: store.getters,
},
});
//Mock props
const mockMixin = {
methods: {
getCmsContent: jest.fn(),
},
};
mountOptions.propsData = {
modelValue: modelValueProp,
};
mountOptions.mixins = [mockMixin];
const wrapper = shallowMount(vehicleQuestion, mountOptions);
return { wrapper };
}

View file

@ -0,0 +1,62 @@
<template>
<dropdownQuestion
:options="values"
disableAutoFill
v-model="selectedValue"
:isDisabled="!values.length" />
</template>
<script>
import dropdownQuestion from "@/digital-components/dropdown-question/dropdown-question";
export default {
name: "vehicle-question",
data() {
return {
values: [],
selectedIndex: null,
};
},
props: {
modelValue: String,
updateValues: Function,
},
components: {
dropdownQuestion,
},
computed: {
selectedValue: {
get() {
return this.selectedIndex?.toString();
},
set(newValue) {
this.selectedIndex = newValue;
newValue = newValue != null && newValue > -1 ? this.values[newValue] : null;
this.$emit("update:modelValue", newValue);
},
},
},
methods: {
async getNewValues(selectedYMMS) {
const results = await this.updateValues();
this.values = results?.data;
if (this.values.length == 1) {
this.selectedValue = 0;
} else {
this.selectedValue =
selectedYMMS != null ? this.values.indexOf(selectedYMMS) : null;
}
},
clearValues() {
this.values = [];
this.selectedValue = null;
},
},
};
</script>

View file

@ -0,0 +1,91 @@
// Components
import vehicle from "@/layouts/vehicle/vehicle.vue";
// Supporting files
import { nextTick } from "vue";
import { mount } from "@vue/test-utils";
describe("vehicle.vue", () => {
test('"Continue" button is enabled after YMMS is selected.', async () => {
const { wrapper } = setupMocks();
const continueButton = wrapper.get('[data-test-id="funnel-footer-main-button"]');
expect(continueButton.attributes()["aria-disabled"]).toBe("false");
});
test("arePagePrerequisitesValid should be true ", async () => {
//Arrange
const { wrapper } = setupMocks();
//Act
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
await nextTick();
//Assert
expect(arePagePrerequisitesValid).toBe(true);
});
});
describe("navigation", () => {
test("forwardButtonAction should trigger navigateForward", async () => {
// Arrange
const { wrapper } = setupMocks();
wrapper.vm.navigateForward = jest.fn();
// Act
await wrapper.vm.forwardButtonAction();
//Assert
expect(wrapper.vm.navigateForward).toHaveBeenCalled();
});
});
const FunnelFooterWidgetMockData = {
ForwardButtonText: "Continue",
};
function setupMocks() {
const mockRoute = {
query: {
fmgPage: "vehicle",
},
};
const mockRouter = {
navigate: jest.fn(),
};
const wrapper = mount(vehicle, {
global: {
mixins: [
{
methods: {
getCmsContent: jest.fn((cmsWidgetName, fieldName) => {
if (cmsWidgetName === "FunnelFooterWidget") {
if (fieldName === "ForwardButtonText") {
return FunnelFooterWidgetMockData.ForwardButtonText;
}
}
return "";
}),
getFooterInfoBoxHeight: jest.fn(() => 80),
},
},
],
mocks: {
$route: mockRoute,
$router: mockRouter,
},
stubs: {
FunnelHeader: true,
FunnelSubHeader: true,
VehicleBanner: true,
},
},
});
return { mockRoute, mockRouter, wrapper };
}

View file

@ -0,0 +1,278 @@
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<div class="page-container-grouped-styles position-relative">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<div class="select-car">
<div class="container-fluid pb-2">
<div class="row">
<div class="col">
<div class="select-car-form rounded">
<funnelSubHeader
cmsWidgetName="FunnelSubHeaderWidget"
class="siteSubHeader" />
<vehicleQuestion
ref="vehicleYearQuestion"
class="mb-2 mt-4"
v-model="selectedYear"
cmsWidgetName="VehicleYearQuestion"
:updateValues="updateYearValues"
validationRules="year-required"
placeHolderText="Select year"
inputId="yearQuestionField" />
<vehicleQuestion
ref="vehicleMakeQuestion"
class="mb-2 mt-4"
v-model="selectedMake"
cmsWidgetName="VehicleMakeQuestion"
:updateValues="updateMakeValues"
validationRules="make-required"
placeHolderText="Select make"
inputId="makeQuestionField" />
<vehicleQuestion
ref="vehicleModelQuestion"
class="mb-2 mt-4"
v-model="selectedModel"
cmsWidgetName="VehicleModelQuestion"
:updateValues="updateModelValues"
validationRules="model-required"
placeHolderText="Select model"
inputId="modelQuestionField" />
<vehicleQuestion
ref="vehicleStyleQuestion"
class="mb-2 mt-4"
v-model="selectedStyle"
cmsWidgetName="VehicleStyleQuestion"
:updateValues="updateStyleValues"
validationRules="style-required"
placeHolderText="Select style"
inputId="styleQuestionField" />
<vehicleBanner
cmsWidgetName="VehicleBannerWidget"
:displayGenericVehicleImage="displayGeneric"
class="mt-5 mb-3"
ref="banner" />
<funnelFooter
cmsWidgetName="FunnelFooterWidget"
@ForwardClicked="forwardButtonAction"
:isForwardActionDisabled="!meta.valid"
ref="funnelFooter" />
</div>
</div>
</div>
</div>
</div>
</div>
</Form>
</template>
<script>
// Components
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner";
import vehicleQuestion from "@/layouts/vehicle/vehicle-question/vehicle-question";
// Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { experimentUniverses } from "@/constants/experiments";
import { getDeviceIdValue, getSessionKeyValue } from "@/helpers/heritage-integration/cookie-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { Form, defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
import store from "@/store";
import { storeActions } from "@/constants/store-actions.js";
import baseMixin from "@/mixins/base-mixin.js";
//define validation rules
defineRule("year-required", required(errorMessages.YEAR_REQUIRED));
defineRule("make-required", required(errorMessages.MAKE_REQUIRED));
defineRule("model-required", required(errorMessages.MODEL_REQUIRED));
defineRule("style-required", required(errorMessages.STYLE_REQUIRED));
export default {
name: "vehicle",
data() {
return {
selectedYear: null,
selectedMake: null,
selectedModel: null,
selectedStyle: null,
};
},
props: {
cmsWidgetName: String,
validationRules: String,
},
mounted() {
this.$refs["vehicleYearQuestion"].getNewValues(this.selectedYearfromStore);
},
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
const experimentForLogging = store.getters.applicationUser.experiments.find(
(e) => e.universeName === experimentUniverses.CONCEPT_FUNNEL
);
// If the concept funnel experiment is found, as it should be when coming from safelite.com, then log the experiment exposure.
if (experimentForLogging !== undefined) {
// Log experiment exposure
baseMixin.methods.dispatchStoreAction(
storeActions.LOG_EXPERIMENT_EXPOSURE,
{
userId: getDeviceIdValue(),
sessionKey: getSessionKeyValue(),
pageName: to.query.fmgPage,
experiment: experimentForLogging,
},
false
);
}
// Settle promises and get results
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},
];
let resultMap = await settleAllPromises(promiseResultMap);
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
});
},
watch: {
selectedYear(year) {
const parsedYear = parseInt(year);
this.dispatchStoreAction(storeActions.SAVE_VEHICLE_YEAR, parsedYear);
if (year) {
this.$refs["vehicleMakeQuestion"].getNewValues(this.selectedMakefromStore);
} else {
this.$refs["vehicleMakeQuestion"].clearValues();
}
},
selectedMake(make) {
this.dispatchStoreAction(storeActions.SAVE_VEHICLE_MAKE, make, false);
if (make) {
this.$refs["vehicleModelQuestion"].getNewValues(this.selectedModelfromStore);
} else {
this.$refs["vehicleModelQuestion"].clearValues();
}
},
selectedModel(model) {
this.dispatchStoreAction(storeActions.SAVE_VEHICLE_MODEL, model, false);
if (model) {
this.$refs["vehicleStyleQuestion"].getNewValues(this.selectedStylefromStore);
} else {
this.$refs["vehicleStyleQuestion"].clearValues();
}
},
selectedStyle(style) {
this.dispatchStoreAction(storeActions.SAVE_VEHICLE_STYLE, style, false);
this.setVehicle();
},
},
methods: {
setVehicle() {
return this.dispatchStoreAction(this.storeActions.SET_VEHICLE, {
year: this.$store.getters.vehicle.year,
make: this.$store.getters.vehicle.make,
model: this.$store.getters.vehicle.model,
style: this.$store.getters.vehicle.style,
});
},
arePagePrerequisitesValid() {
return true;
},
async forwardButtonAction() {
return this.navigateForward();
},
navigateForward() {
this.$router.navigateWithSaving(this.navigationScenarios.CLICKED_FORWARD, this.$route);
},
async updateYearValues() {
return await baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_YEARS, {});
},
async updateMakeValues() {
return await baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_MAKES, {
year: store.getters.vehicle.year,
});
},
async updateModelValues() {
return await baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_MODELS, {
year: store.getters.vehicle.year,
make: store.getters.vehicle.make,
});
},
async updateStyleValues() {
return baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_STYLES, {
year: store.getters.vehicle.year,
make: store.getters.vehicle.make,
model: store.getters.vehicle.model,
});
},
},
computed: {
displayGeneric() {
return !this.selectedStyle;
},
selectedYearfromStore() {
return store.getters.vehicle.year;
},
selectedMakefromStore() {
return store.getters.vehicle.make;
},
selectedModelfromStore() {
return store.getters.vehicle.model;
},
selectedStylefromStore() {
return store.getters.vehicle.style;
},
},
components: {
funnelHeader,
funnelFooter,
funnelSubHeader,
vehicleBanner,
Form,
vehicleQuestion,
},
};
</script>
<style lang="scss">
.select-car-form {
margin-left: 0.75rem;
margin-right: 0.75rem;
}
.siteSubHeader {
margin-top: 1.5rem;
}
</style>

View file

@ -59,7 +59,7 @@
v-model="emailAddress"
customInputId="emailAddress"
isRequired
validationRules="email-address-format" />
validationRules="email-address-required|email-address-format" />
</div>
</div>
<div class="row mb-0">
@ -79,7 +79,12 @@
:manualHeadline="AlertPerfectMatchInsuranceVerifiedHeader"
:manualCopy="AlertPerfectMatchInsuranceVerifiedBody"
v-model="customAlertData"
v-if="vinPopulatedOnPageLoad && isInsuranceVerified"
v-if="
vinPopulatedOnPageLoad &&
isInsuranceVerified &&
!displayInvalidZipAlert &&
!displayNonServiceableZipAlert
"
alertClass="alert-success" />
<alert
class="my-4"
@ -106,7 +111,12 @@
:manualHeadline="AlertPerfectMatchInsuranceNotVerifiedHeader"
:manualCopy="AlertPerfectMatchInsuranceNotVerifiedBody"
v-model="customAlertData"
v-if="vinPopulatedOnPageLoad && !isInsuranceVerified"
v-if="
vinPopulatedOnPageLoad &&
!isInsuranceVerified &&
!displayInvalidZipAlert &&
!displayNonServiceableZipAlert
"
alertClass="alert-success" />
<funnelFooter
cmsWidgetName="FunnelFooterWidget"
@ -152,6 +162,7 @@ import vinPagesMixin from "@/mixins/vin-pages-mixin";
// DEFINE VALIDATION RULES
defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED));
defineRule("zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT));
defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
defineRule(
"email-address-format",
regex(

View file

@ -176,6 +176,18 @@ export default {
getSessionIdValue() === "00000000-0000-0000-0000-000000000000"
);
},
removeParamsFromEndpoint(endpoint) {
const endpointWithoutParams = endpoint.split("?")[0];
const numSlashesBeforeParams = 6;
let splitString = endpointWithoutParams.split("/");
if (splitString.length > numSlashesBeforeParams) {
splitString = splitString.slice(0, numSlashesBeforeParams);
return splitString.join("/");
} else {
return endpointWithoutParams;
}
},
},
computed: {
analyticsPageEvents() {

View file

@ -3,6 +3,7 @@ const fmgPageValues = {
VEHICLE_MAKE: "vehicle-make",
VEHICLE_MODEL: "vehicle-model",
VEHICLE_STYLE: "vehicle-style",
VEHICLE: "vehicle",
VEHICLE_DAMAGE: "vehicle-damage",
ADDRESS_LOOKUP: "address-lookup",
VIN_LOOKUP: "vin-lookup",

View file

@ -4,6 +4,15 @@ import { navigationScenarios } from "@/router/router-constants/navigation-scenar
// Get store from router/index.js instead of importing it here to get updated values
const routingTable = function (store) {
return [
{
fmgPageValue: fmgPageValues.VEHICLE,
maps: [
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
},
],
},
{
fmgPageValue: fmgPageValues.VEHICLE_YEAR,
maps: [
@ -57,7 +66,7 @@ const routingTable = function (store) {
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationFmgPageValue: fmgPageValues.VEHICLE_STYLE,
destinationFmgPageValue: fmgPageValues.VEHICLE,
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_VIN,
@ -457,7 +466,7 @@ const routingTable = function (store) {
maps: [
{
scenario: navigationScenarios.CLICKED_VEHICLE_EDIT,
destinationFmgPageValue: fmgPageValues.VEHICLE_YEAR,
destinationFmgPageValue: fmgPageValues.VEHICLE,
},
{
scenario: navigationScenarios.CLICKED_DAMAGE_EDIT,

View file

@ -534,6 +534,9 @@ export const getters = {
);
return !!nonWindshieldItems?.length;
},
isMobileAppointment: (state) => {
return state.order.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE;
},
isRecalibrationOnOrder: (state) => {
return getHasRecalibrationPart(state);
},
@ -615,6 +618,10 @@ function getNonFalseValuesOfPropertyInArrayOfObjects(array, propertyName) {
return (array ?? []).map((x) => x[propertyName]).filter((x) => x);
}
function provisionalTriggersToString(provisionalTriggers) {
return "ProvisionalTriggers:" + provisionalTriggers.join(",");
}
// Export Actions
export const actions = {
// Vehicle API Actions
@ -1239,13 +1246,16 @@ export const actions = {
const order = context.state.order;
const vehicle = context.state.order.vehicle;
let partNumbers = [
let lineItems = [
...(order.lineItems.supportingItems ?? []),
...(order.lineItems.vaps ?? []),
...getFlattenedArrayOfLineItemsWithChildParts(order.lineItems.glassParts),
];
partNumbers = partNumbers.map((lineItem) => {
return lineItem.partNumber;
lineItems = lineItems.map((lineItem) => {
return {
partNumber: lineItem.partNumber,
partType: lineItem.partType,
};
});
const glassPieces = order.damage.glassToReplace
? convertGlassPieceToBackEndCompatibleFormat(order.damage.glassToReplace)
@ -1258,7 +1268,7 @@ export const actions = {
applicationName: applicationConfig.APPLICATION_NAME,
parentAccountNumber: context.getters.payment.parentAccountNumber,
carId: vehicle.carId,
partNumbers: partNumbers,
lineItems: lineItems,
glassPieces: glassPieces,
eon: order.eon,
coverage: {
@ -1267,11 +1277,12 @@ export const actions = {
additionalAuthFlag: "",
},
partSelection: {
// TODO: Provisional booking will utilize these fields
hasAnsweredPartQuestions: false,
hasAnsweredMoldingQuestions: false,
hasAnsweredCapabilityQuestions: false,
hasManuallySelectedParts: false,
hasAnsweredPartQuestions: !!order.damage.partQuestionAnswers?.length,
hasAnsweredMoldingQuestions: !!order.damage.moldingQuestionAnswers?.length,
hasAnsweredCapabilityQuestions: !!order.damage.capabilityQuestionAnswers?.length,
hasManuallySelectedParts:
!!context.state.applicationUser.pageData["vehicle-parts"]?.partsOrQuestions
.length,
},
vehicle: {
year: vehicle.year,
@ -1286,19 +1297,24 @@ export const actions = {
method: endpoints.GetShopTimeSlots.method,
endpoint: endpoints.GetShopTimeSlots.url,
payload: payload,
additionalSuccessEventDataHandler: (response) =>
provisionalTriggersToString(response.data.provisionalTriggers),
});
},
getMobileTimeSlots(context, { startDate, endDate }) {
const order = context.state.order;
const vehicle = context.state.order.vehicle;
let partNumbers = [
let lineItems = [
...(order.lineItems.supportingItems ?? []),
...(order.lineItems.vaps ?? []),
...getFlattenedArrayOfLineItemsWithChildParts(order.lineItems.glassParts),
];
partNumbers = partNumbers.map((lineItem) => {
return lineItem.partNumber;
lineItems = lineItems.map((lineItem) => {
return {
partNumber: lineItem.partNumber,
partType: lineItem.partType,
};
});
const glassPieces = order.damage.glassToReplace
? convertGlassPieceToBackEndCompatibleFormat(order.damage.glassToReplace)
@ -1309,7 +1325,7 @@ export const actions = {
applicationName: applicationConfig.APPLICATION_NAME,
parentAccountNumber: context.getters.payment.parentAccountNumber,
carId: vehicle.carId,
partNumbers: partNumbers,
lineItems: lineItems,
glassPieces: glassPieces,
eon: order.eon,
coverage: {
@ -1318,11 +1334,12 @@ export const actions = {
additionalAuthFlag: "",
},
partSelection: {
// TODO: Provisional booking will utilize these fields
hasAnsweredPartQuestions: false,
hasAnsweredMoldingQuestions: false,
hasAnsweredCapabilityQuestions: false,
hasManuallySelectedParts: false,
hasAnsweredPartQuestions: !!order.damage.partQuestionAnswers?.length,
hasAnsweredMoldingQuestions: !!order.damage.moldingQuestionAnswers?.length,
hasAnsweredCapabilityQuestions: !!order.damage.capabilityQuestionAnswers?.length,
hasManuallySelectedParts:
!!context.state.applicationUser.pageData["vehicle-parts"]?.partsOrQuestions
.length,
},
vehicle: {
year: vehicle.year,
@ -1337,6 +1354,8 @@ export const actions = {
method: endpoints.GetMobileTimeSlots.method,
endpoint: endpoints.GetMobileTimeSlots.url,
payload: payload,
additionalSuccessEventDataHandler: (response) =>
provisionalTriggersToString(response.data.provisionalTriggers),
});
},
@ -1391,7 +1410,7 @@ export const actions = {
},
},
customer: {
emailAddress: order.customer.emailAddress || null,
emailAddress: order.customer.emailAddress,
},
damage: {
numberOfChips: damage.numberOfChips,
@ -1960,7 +1979,7 @@ export const actions = {
},
saveEmail(context, email) {
context.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, email);
context.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, email === "" ? null : email);
},
saveVin(context, { isSelectedGlassAvailableForVehicle, vehicleInfo }) {
@ -2199,6 +2218,7 @@ async function resetScheduleIfUnavailable(context, order) {
// If you put any kind of time stamp on the date string with dashes, then it IS parsed as local time.
var aptDate = new Date(order.schedule.date + "T00:00:00");
var curDate = new Date();
curDate.setHours(0, 0, 0, 0);
// if appointment date is in the past, clear schedule
if (aptDate.getTime() < curDate.getTime()) {

View file

@ -4,6 +4,7 @@ import { storeMutations } from "@/constants/store-mutations";
import { storeActions } from "@/constants/store-actions";
import { experimentTriggers } from "@/constants/experiments";
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
// Mock global method
globalMethods.callHttpClient = jest.fn();
@ -2869,6 +2870,45 @@ describe("Getters", () => {
expect(getters.payment(storeState).insuranceCoverage.isVerified).toEqual(true);
});
describe("isMobileAppointment", () => {
it("Should return true for mobile appointments", () => {
// Arrange
const storeState = state;
// Act
mutations.updateServiceLocation(storeState, {
appointmentType: AppointmentTypeStrings.MOBILE,
});
// Assert
expect(getters.isMobileAppointment(storeState)).toBe(true);
});
it("Should return false for non-mobile appointments", () => {
// Arrange
const storeState = state;
// Act
mutations.updateServiceLocation(storeState, {
appointmentType: AppointmentTypeStrings.IN_SHOP,
});
// Assert
expect(getters.isMobileAppointment(storeState)).toBe(false);
});
it("Should return false for null appointments", () => {
// Arrange
const storeState = state;
// Act
mutations.updateServiceLocation(storeState, { appointmentType: null });
// Assert
expect(getters.isMobileAppointment(storeState)).toBe(false);
});
});
describe("experimentOrder", () => {
test("glassToReplace, glassParts, and otherParts are null > return correct experimentOrder values", () => {
// Arrange

View file

@ -100,7 +100,7 @@ export default {
color: $white;
background: $blue-700;
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
pointer-events: none;
pointer-events: auto;
}
&.delay {
// fixes flicker while transitioning between states

View file

@ -3,7 +3,11 @@
class="loader"
role="alert"
aria-label="Loading new page"
v-bind:class="[this.loaderColor, this.loaderPosition, this.blockUi]"></div>
v-bind:class="[
this.loaderColor,
this.loaderPosition,
this.allowPageInteraction ? 'allow-ui-interaction' : '',
]"></div>
</template>
<script>
@ -19,10 +23,11 @@ export default {
loaderPosition: {
type: String,
},
/* Controls whether we block the UI when the loader is active or now, defaults to true*/
blockUi: {
/* Set to true for loaders where UI interaction must be allowed to continue while the loader is active
(See example shop-list-button) */
allowPageInteraction: {
type: Boolean,
default: true,
default: false,
},
},
};
@ -31,6 +36,7 @@ export default {
<style lang="scss">
.loader {
display: flex;
pointer-events: all;
//Open an overlay to prevent page interaction
&:before {
@ -87,8 +93,8 @@ export default {
&.black:after {
background-color: $black;
}
&.no-block::before {
&.allow-ui-interaction::before {
//no-block to enable clicking on certain buttons with loaders while the loader is actives
z-index: -1;
}
}