Merge branch 'develop' into CSR-906-update-textbox-question-for-service-location
This commit is contained in:
commit
a3ca0b8124
37 changed files with 987 additions and 465 deletions
|
|
@ -28,7 +28,7 @@ resources:
|
|||
type: github
|
||||
name: Safelite/AzureDevOps
|
||||
endpoint: Safelite
|
||||
ref: refs/tags/t5.5.35
|
||||
ref: refs/tags/t5.5.40
|
||||
|
||||
variables:
|
||||
- group: Digital-Infrastructure
|
||||
|
|
@ -109,6 +109,58 @@ stages:
|
|||
paths: /fmg/*
|
||||
awsProfile: $(devDeploymentProfile)
|
||||
|
||||
# Test Build/Deploy
|
||||
- stage: Test
|
||||
condition: eq(variables['Build.SourceBranch'], variables['test-branch'] )
|
||||
variables:
|
||||
- group: FixMyGlassTest
|
||||
jobs:
|
||||
- deployment: testBuildDeployment
|
||||
displayName: Build and Deploy FMG - Test
|
||||
environment: digitalCloud-sys
|
||||
container: node
|
||||
workspace:
|
||||
clean: all
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- checkout: self
|
||||
clean: true
|
||||
- template: templates/digital/step-build-vue.yml@AzureDevOps
|
||||
parameters:
|
||||
buildOutputDir: dist
|
||||
environment: Test
|
||||
- template: templates/digital/step-deploy-vue.yml@AzureDevOps
|
||||
parameters:
|
||||
artifactName: vueDistTest
|
||||
awsProfile: $(sysDeploymentProfile)
|
||||
outputPath: /fmg/
|
||||
deployBuckets:
|
||||
safelite-sys-fmg-us-east-1:
|
||||
clearFolder: true
|
||||
deployFolder: ""
|
||||
region: us-east-1
|
||||
safelite-sys-fmg-us-east-2:
|
||||
clearFolder: true
|
||||
deployFolder: ""
|
||||
region: us-east-2
|
||||
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: $(vueCfDistributionId)
|
||||
- template: templates/digital/invalidate-cloudfront-cache.yml@AzureDevOps
|
||||
parameters:
|
||||
awsCliContainer: awscli
|
||||
distributionId: $(apiCfDistributionId)
|
||||
paths: /fmg/*
|
||||
awsProfile: $(sysDeploymentProfile)
|
||||
|
||||
# QA Build/Deploy
|
||||
- stage: Qa
|
||||
condition: eq(variables['Build.SourceBranch'], variables['qa-branch'] )
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ module.exports = {
|
|||
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
statements: 83,
|
||||
statements: 82,
|
||||
// Got the go ahead from Mark to temporarily lower this. Taking out initialize component made the year,make,model and style coverage drop a bit. Once unit tests for license plate lookup, vin lookup and address lookup are in the coverage should go back up to 90
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -23,4 +23,5 @@ export default {
|
|||
@import "@/styles/common-typography-styles.scss";
|
||||
@import "@/styles/common-error-styles.scss";
|
||||
@import "@/styles/common-animations.scss";
|
||||
@import "@/styles/shared-input-button-styles.scss";
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@ describe("baseInputButton.vue", () => {
|
|||
expect(wrapper.emitted()).not.toHaveProperty("update:modelValue");
|
||||
});
|
||||
|
||||
test("focus and click enter on a checkbox => update:modelValue is emitted with correct value", async () => {
|
||||
test("focus and click enter on a checkbox => nothing should happen", async () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({
|
||||
mockData: {
|
||||
|
|
@ -243,9 +243,10 @@ describe("baseInputButton.vue", () => {
|
|||
|
||||
// Act
|
||||
await input.trigger("keypress", { key: "enter" });
|
||||
console.log(wrapper.emitted()["update:modelValue"]);
|
||||
|
||||
// Assert
|
||||
expect(wrapper.emitted()["update:modelValue"][0][0]).toEqual(["Hi"]);
|
||||
expect(wrapper.emitted()["update:modelValue"]).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -319,7 +320,7 @@ describe("baseInputButton.vue", () => {
|
|||
expect(wrapper.vm.handlePushClickEventToGACheck).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("eventType === eventTypes.ENTER => call handleClick and handlePushClickEventToGACheck", () => {
|
||||
test("eventType === eventTypes.ENTER => do nothing", () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({
|
||||
mockData: {
|
||||
|
|
@ -336,11 +337,9 @@ describe("baseInputButton.vue", () => {
|
|||
wrapper.vm.handleEventAction("enter", { myEvent: "test" });
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.handleClick).toHaveBeenCalledWith({
|
||||
myEvent: "test",
|
||||
});
|
||||
expect(wrapper.vm.handlePushClickEventToGACheck).toHaveBeenCalledWith("click");
|
||||
expect(wrapper.vm.handleClick).not.toHaveBeenCalled();
|
||||
expect(wrapper.vm.handleSelectionChange).not.toHaveBeenCalled();
|
||||
expect(wrapper.vm.handlePushClickEventToGACheck).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("eventType === eventTypes.CHANGE => call handleClick and handlePushClickEventToGACheck", () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<label
|
||||
:class="[buttonWrapperClasses, { 'has-error': errors.length > 0 }]"
|
||||
:class="[buttonWrapperClasses, { 'has-error': errors.length > 0, selected: isChecked }]"
|
||||
:for="buttonId"
|
||||
@focusin="handleFocus"
|
||||
@focusout="handleBlur"
|
||||
|
|
@ -53,7 +53,6 @@ export default {
|
|||
handleEventAction(eventType, e) {
|
||||
if (this.isMultiSelect) {
|
||||
switch (eventType) {
|
||||
case this.eventTypes.ENTER:
|
||||
case this.eventTypes.CHANGE:
|
||||
this.handleClick(e);
|
||||
this.handlePushClickEventToGACheck(this.eventTypes.CLICK);
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ describe("buttonQuestion.vue", () => {
|
|||
// Act
|
||||
const wrapper = shallowMount(buttonQuestion, {
|
||||
propsData: {
|
||||
buttonType: "listCard",
|
||||
buttonTypeString: "listCard",
|
||||
groupName: "group-name",
|
||||
},
|
||||
});
|
||||
|
|
@ -46,7 +46,7 @@ describe("buttonQuestion.vue", () => {
|
|||
// Act
|
||||
const wrapper = shallowMount(buttonQuestion, {
|
||||
propsData: {
|
||||
buttonType: "listButtonHorizontal",
|
||||
buttonTypeString: "listButtonHorizontal",
|
||||
groupName: "group-name",
|
||||
},
|
||||
});
|
||||
|
|
@ -61,7 +61,7 @@ describe("buttonQuestion.vue", () => {
|
|||
// Act
|
||||
const wrapper = shallowMount(buttonQuestion, {
|
||||
propsData: {
|
||||
buttonType: "radio",
|
||||
buttonTypeString: "radio",
|
||||
groupName: "group-name",
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -33,9 +33,12 @@
|
|||
v-for="answer in buttonsInfo"
|
||||
:key="answer.value ? answer.value : answer">
|
||||
<component
|
||||
:is="buttonType"
|
||||
:is="buttonTypeString"
|
||||
:buttonLabel="answer.buttonLabel"
|
||||
:buttonLabelSubCopy="answer.buttonLabelSubCopy"
|
||||
:buttonBodyCopy="answer.buttonBodyCopy"
|
||||
:buttonAuxillaryCopy="answer.buttonAuxillaryCopy"
|
||||
:buttonFooterCopy="answer.buttonFooterCopy"
|
||||
:buttonImage="answer.buttonImage"
|
||||
:buttonImageId="answer.buttonImageId"
|
||||
:groupName="answer.groupName"
|
||||
|
|
@ -45,6 +48,7 @@
|
|||
:isWide="isWide"
|
||||
:validationRules="validationRules"
|
||||
:textPosition="textPosition"
|
||||
:additionalButtonStyling="additionalButtonStyling"
|
||||
:lastValuePushedToGa="lastValuePushedToGa"
|
||||
:setLastValuePushedToGa="setLastValuePushedToGa"
|
||||
v-model="selectedValues" />
|
||||
|
|
@ -78,10 +82,14 @@ import { ErrorMessage } from "vee-validate";
|
|||
export default {
|
||||
name: "buttonQuestion",
|
||||
props: {
|
||||
buttonType: {
|
||||
buttonTypeString: {
|
||||
type: String,
|
||||
default: "listButton",
|
||||
},
|
||||
buttonTypeObject: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
isMultiSelect: Boolean,
|
||||
groupName: String,
|
||||
questionText: String,
|
||||
|
|
@ -109,6 +117,12 @@ export default {
|
|||
suppressError: Boolean,
|
||||
useTextForValue: Boolean,
|
||||
valueToLogType: String,
|
||||
additionalButtonStyling: String,
|
||||
},
|
||||
beforeMount() {
|
||||
if (this.buttonTypeObject) {
|
||||
this.$options.components[this.buttonTypeString] = this.buttonTypeObject;
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -119,7 +133,7 @@ export default {
|
|||
getFieldSetClasses() {
|
||||
if (this.isOverflowScrollable) {
|
||||
return "container-fluid overflow-scroll position-absolute px-5 pt-1 py-0";
|
||||
} else if (this.buttonType == "listCard") {
|
||||
} else if (this.buttonTypeString == "listCard") {
|
||||
return "w-100";
|
||||
} else {
|
||||
return "";
|
||||
|
|
@ -127,7 +141,7 @@ export default {
|
|||
},
|
||||
getComponentLoopWrapperClasses() {
|
||||
let classes;
|
||||
switch (this.buttonType) {
|
||||
switch (this.buttonTypeString) {
|
||||
case "listButton":
|
||||
classes = "w-100";
|
||||
break;
|
||||
|
|
@ -143,6 +157,9 @@ export default {
|
|||
case "radio":
|
||||
classes = "ui-radio d-flex";
|
||||
break;
|
||||
case "servicePackageRadio":
|
||||
classes = "package-main";
|
||||
break;
|
||||
}
|
||||
return classes;
|
||||
},
|
||||
|
|
@ -151,8 +168,10 @@ export default {
|
|||
|
||||
classes += this.isWide ? "col-12" : "col";
|
||||
|
||||
if (this.buttonType == "radio") {
|
||||
if (this.buttonTypeString == "radio") {
|
||||
classes += " radio-button-container";
|
||||
} else if (this.buttonTypeString == "servicePackageRadio") {
|
||||
classes = "package-wrapper";
|
||||
}
|
||||
|
||||
return classes;
|
||||
|
|
@ -162,6 +181,9 @@ export default {
|
|||
buttonLabel: answer.buttonLabel ?? answer.Text ?? answer,
|
||||
altText: answer.altText ?? (answer.Name ? answer.Name : answer),
|
||||
buttonLabelSubCopy: answer.buttonLabelSubCopy ?? answer.SubText,
|
||||
buttonBodyCopy: answer.buttonBodyCopy ?? answer.buttonBodyCopy,
|
||||
buttonAuxillaryCopy: answer.buttonAuxillaryCopy ?? answer.buttonAuxillaryCopy,
|
||||
buttonFooterCopy: answer.buttonFooterCopy ?? answer.buttonFooterCopy,
|
||||
buttonImage: answer.buttonImage ?? answer.AnswerImageUrl,
|
||||
buttonImageId: answer.buttonImageId ?? answer.ImageId,
|
||||
groupName: this.formatString(this.groupName),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
<template>
|
||||
<div
|
||||
class="dropdown-question"
|
||||
:class="(errors && errors.length) || hasError ? 'has-error' : ''">
|
||||
<div class="dropdown-question" :class="errorMessage || hasError ? 'has-error' : ''">
|
||||
<label
|
||||
:for="inputId"
|
||||
:aria-label="questionText"
|
||||
|
|
@ -43,6 +41,7 @@ export default {
|
|||
disableAutoFill: Boolean,
|
||||
validationRules: String,
|
||||
cmsWidgetName: String,
|
||||
hasError: Boolean,
|
||||
},
|
||||
setup(props) {
|
||||
const propsClone = Object.assign({}, props);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ const dynamicStrings = {
|
|||
GLOBAL_STATE: "globalState",
|
||||
CUSTOM: "custom",
|
||||
ROUTER_LINK: "routerLink:",
|
||||
TEXT_LINK: "textLink:",
|
||||
};
|
||||
|
||||
export { dynamicStrings };
|
||||
|
|
|
|||
8
src/constants/part-type-strings.js
Normal file
8
src/constants/part-type-strings.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
const partTypeStrings = {
|
||||
FRONT_WIPER: "FRONT WIPER",
|
||||
REAR_WIPER: "REAR WIPER",
|
||||
RAIN_DEFENSE: "RAIN DEFENSE",
|
||||
RECALIBRATION: "RECALIBRATION",
|
||||
};
|
||||
|
||||
export { partTypeStrings };
|
||||
|
|
@ -31,7 +31,7 @@ export function fetchCmsContentForPage(fmgPage) {
|
|||
// Function to convert a string, into a matching global state item.
|
||||
function mapStringToState(str) {
|
||||
// Pull all matches out of the string.
|
||||
const regexExp = new RegExp("{(.*?):(.*?)}", "g");
|
||||
const regexExp = new RegExp("{([^{}]*?):([^{}]*?)}", "g");
|
||||
const regexMatches = [...str.matchAll(regexExp)];
|
||||
const globalStateMatches = regexMatches.filter((match) => {
|
||||
return match[1] === dynamicStrings.GLOBAL_STATE;
|
||||
|
|
@ -39,20 +39,13 @@ function mapStringToState(str) {
|
|||
|
||||
// Our final string value that will be built from the matches.
|
||||
let stringBuilder = "";
|
||||
|
||||
for (const match of globalStateMatches) {
|
||||
// Reset store state for each match.
|
||||
let storeState = store.state;
|
||||
|
||||
for (const s of match[2].split(".")) {
|
||||
if (storeState[s] != undefined) {
|
||||
storeState = storeState[s];
|
||||
} else {
|
||||
return ""; // if we can't map our string to state data, return an empty string.
|
||||
}
|
||||
const valueFromStore = getStoreValueFromString(match[2]);
|
||||
if (!valueFromStore) {
|
||||
return ""; // if we can't map our string to state data, return an empty string.
|
||||
}
|
||||
|
||||
const stringWithReplacement = str.replace(match[0], storeState);
|
||||
const stringWithReplacement = str.replace(match[0], valueFromStore);
|
||||
|
||||
// If we still have values we need to substitute, call this function again.
|
||||
if (stringWithReplacement.includes(dynamicStrings.GLOBAL_STATE)) {
|
||||
|
|
@ -62,7 +55,6 @@ function mapStringToState(str) {
|
|||
// Concatenate the string.
|
||||
stringBuilder = `${stringBuilder} ${stringWithReplacement}`;
|
||||
}
|
||||
|
||||
return stringBuilder.trimStart();
|
||||
}
|
||||
|
||||
|
|
@ -88,6 +80,11 @@ function findAndReplaceGlobalStateValues(widgetModel, widgetName) {
|
|||
function processWidgetItemForReplacement(widgetModel, key) {
|
||||
// If we have a string, and it needs to be replaced.
|
||||
if (typeof widgetModel[key] === "string") {
|
||||
widgetModel[key] = processIfStatements(
|
||||
widgetModel[key],
|
||||
dynamicStrings.GLOBAL_STATE,
|
||||
getStoreValueFromString
|
||||
);
|
||||
if (widgetModel[key].includes(dynamicStrings.GLOBAL_STATE)) {
|
||||
widgetModel[key] = mapStringToState(widgetModel[key]);
|
||||
}
|
||||
|
|
@ -107,23 +104,217 @@ function processWidgetItemForReplacement(widgetModel, key) {
|
|||
return widgetModel[key];
|
||||
}
|
||||
|
||||
function getStoreValueFromString(str) {
|
||||
let storeOrStateObject = str.includes("getters") ? store : store.state;
|
||||
for (const s of str.split(".")) {
|
||||
if (storeOrStateObject[s] != undefined) {
|
||||
storeOrStateObject = storeOrStateObject[s];
|
||||
} else {
|
||||
return ""; // if we can't map our string to state data, return an empty string.
|
||||
}
|
||||
}
|
||||
return storeOrStateObject;
|
||||
}
|
||||
|
||||
///////////////////////////////////
|
||||
// If Statement Processing Logic //
|
||||
///////////////////////////////////
|
||||
|
||||
/**
|
||||
* Recursive function - replaces all instances of if statements from the CMS that utilize the specified ifConditionKeyword
|
||||
* @param {*} str string - Input string to be processed
|
||||
* @param {*} ifConditionKeyword string - Defines which if statements to process ex: 'globalState'
|
||||
* @param {*} replacePlaceholderCallback function - Callback to replace CMS placeholder values
|
||||
* @returns The processed string
|
||||
*/
|
||||
export function processIfStatements(str, ifConditionKeyword, replacePlaceholderCallback) {
|
||||
const containsRelevantIfStatement = new RegExp("{if:" + ifConditionKeyword + ":.+?}", "g").test(
|
||||
str
|
||||
);
|
||||
if (!containsRelevantIfStatement) {
|
||||
return str;
|
||||
} else {
|
||||
const ifStatementRegexExpression = getIfStatementRegexExpression();
|
||||
const ifStatementRegexMatches = [...str.matchAll(ifStatementRegexExpression)];
|
||||
const completeIfStatementArray = getAndFlagFirstNonNestedIfStatementWithKeyword(
|
||||
ifStatementRegexMatches,
|
||||
ifConditionKeyword
|
||||
);
|
||||
executeIfStatementAndSetProcessedStrings(
|
||||
completeIfStatementArray,
|
||||
replacePlaceholderCallback
|
||||
);
|
||||
const reconstructedPostProcessedString = joinProcessedRegexArray(ifStatementRegexMatches);
|
||||
return processIfStatements(
|
||||
reconstructedPostProcessedString,
|
||||
ifConditionKeyword,
|
||||
replacePlaceholderCallback
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getAndFlagFirstNonNestedIfStatementWithKeyword(matches, ifConditionKeyword) {
|
||||
let index = 0;
|
||||
for (const match of matches) {
|
||||
if (match.groups.isIfStatement && match.groups.ifConditionType === ifConditionKeyword) {
|
||||
let interiorIndex = 0;
|
||||
let nestedLevel = 0;
|
||||
let elseStatementIndex = null;
|
||||
for (const interiorMatch of matches.slice(index + 1)) {
|
||||
if (interiorMatch.groups.isIfStatement) {
|
||||
if (interiorMatch.groups.ifConditionType === ifConditionKeyword) {
|
||||
break;
|
||||
} else {
|
||||
nestedLevel++;
|
||||
}
|
||||
} else if (interiorMatch.groups.isElseStatement) {
|
||||
if (!nestedLevel) {
|
||||
elseStatementIndex = interiorIndex + 1;
|
||||
}
|
||||
} else if (interiorMatch.groups.isEndStatement) {
|
||||
if (nestedLevel) {
|
||||
nestedLevel--;
|
||||
} else {
|
||||
const ifStatementArray = matches.slice(index, index + interiorIndex + 2);
|
||||
flagMatchesForProcessing(ifStatementArray, elseStatementIndex);
|
||||
return ifStatementArray;
|
||||
}
|
||||
}
|
||||
interiorIndex++;
|
||||
}
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
function flagMatchesForProcessing(matches, elseStatementIndex) {
|
||||
matches[0].isFlaggedForProcessing = true;
|
||||
matches[matches.length - 1].isFlaggedForProcessing = true;
|
||||
if (elseStatementIndex) {
|
||||
matches[elseStatementIndex].isFlaggedForProcessing = true;
|
||||
}
|
||||
}
|
||||
|
||||
function joinProcessedRegexArray(regexMatches) {
|
||||
let processedString = "";
|
||||
regexMatches.forEach((match) => {
|
||||
const rawString = match[0];
|
||||
processedString += match.groups.processedString ?? rawString;
|
||||
});
|
||||
return processedString;
|
||||
}
|
||||
|
||||
function executeIfStatementAndSetProcessedStrings(ifStatementArray, replacePlaceholderCallback) {
|
||||
const ifCondition = replacePlaceholderCallback(ifStatementArray[0].groups.ifCondition);
|
||||
let isInsideDesiredBlock = ifCondition;
|
||||
|
||||
ifStatementArray.forEach((entry) => {
|
||||
if (entry.groups.isElseStatement && entry.isFlaggedForProcessing) {
|
||||
isInsideDesiredBlock = !ifCondition;
|
||||
} else if (entry.groups.isEndStatement && entry.isFlaggedForProcessing) {
|
||||
isInsideDesiredBlock = true;
|
||||
}
|
||||
setProcessedStringOnEntry(entry, isInsideDesiredBlock);
|
||||
});
|
||||
}
|
||||
|
||||
function setProcessedStringOnEntry(entry, isInsideDesiredBlock) {
|
||||
if (!isInsideDesiredBlock) {
|
||||
entry.groups.processedString = "";
|
||||
} else {
|
||||
if (entry.groups.isIfStatement) {
|
||||
entry.groups.processedString = entry.isFlaggedForProcessing
|
||||
? entry.groups.ifTrailingString
|
||||
: entry[0];
|
||||
} else if (entry.groups.isElseStatement) {
|
||||
entry.groups.processedString = entry.isFlaggedForProcessing
|
||||
? entry.groups.elseTrailingString
|
||||
: entry[0];
|
||||
} else {
|
||||
entry.groups.processedString = entry.isFlaggedForProcessing
|
||||
? entry.groups.endTrailingString
|
||||
: entry[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getIfStatementRegexExpression() {
|
||||
// Matches but does not capture:
|
||||
// {if:...} or {else} or {end}
|
||||
const anyLogicOperatorNonCapture = "(?:{(?:end|else|if:.*?)})";
|
||||
// NOTE: ?<variableName> syntax stores the captured match like so: match.groups.variableName
|
||||
const matchStartOfString =
|
||||
"(?<processedString>^.+?)" + // Match and Capture all characters (lazy), cannot be empty
|
||||
"(?=(?:{if))"; // Looks ahead but does not capture {if
|
||||
const matchIfOperator =
|
||||
"(?<isIfStatement>{if:)" + // Match & Capture {if:
|
||||
"(?<ifConditionType>.*?):" + // Match all chars up to and including next ":" - Capture all chars up to ":"
|
||||
"(?<ifCondition>.*?)}" + // Match all chars up to and including next "}" - Capture all chars up to "}"
|
||||
"(?<ifTrailingString>.*?)" + // Match and Capture all characters (lazy), can be empty
|
||||
"(?=" +
|
||||
anyLogicOperatorNonCapture +
|
||||
")"; // Looks ahead but does not capture the next logic operator
|
||||
const matchElseOperator =
|
||||
"(?<isElseStatement>{else})" + // Match & Capture {else}
|
||||
"(?<elseTrailingString>.*?)" + // Match & Capture all characters (lazy), can be empty
|
||||
"(?=" +
|
||||
anyLogicOperatorNonCapture +
|
||||
")"; // Looks ahead but does not capture the next logic operator
|
||||
const matchEndOperator =
|
||||
"(?<isEndStatement>{end})" + // Match & Capture {end}
|
||||
"(?<endTrailingString>.*?)" + // Match & Capture all chracters (lazy), can be empty
|
||||
"(?=" +
|
||||
anyLogicOperatorNonCapture +
|
||||
"|$)"; // Looks ahead but does not capture the next logic operator
|
||||
// Combine all matching patterns, separated by "or" pipes
|
||||
return new RegExp(
|
||||
matchStartOfString +
|
||||
"|" +
|
||||
matchIfOperator +
|
||||
"|" +
|
||||
matchElseOperator +
|
||||
"|" +
|
||||
matchEndOperator,
|
||||
"g"
|
||||
);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////
|
||||
// End of If Statement Processing Logic //
|
||||
//////////////////////////////////////////
|
||||
|
||||
export function doesCopyContainRouterLink(copy) {
|
||||
return copy.includes(this.dynamicStrings.ROUTER_LINK);
|
||||
}
|
||||
|
||||
export function doesCopyContainTextLink(copy) {
|
||||
return copy.includes(dynamicStrings.TEXT_LINK);
|
||||
}
|
||||
|
||||
/**
|
||||
* splits copy on { ... } such as {routerlink: ...}
|
||||
* @returns array of strings
|
||||
*/
|
||||
export function splitCopyOnCMSPlaceHolder(copy) {
|
||||
// splits copy on { ... } such as {routerlink: ...}
|
||||
return copy.split(/{(.*?)}/g);
|
||||
}
|
||||
|
||||
export function getRouterLinkRouteFromCopy(copy) {
|
||||
/**
|
||||
* Returns string2 of input following this pattern: {string1:string2,string3}
|
||||
* @returns string
|
||||
*/
|
||||
export function getLinkTargetFromCopy(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];
|
||||
}
|
||||
|
||||
export function getRouterLinkDisplayTextFromCopy(copy) {
|
||||
/**
|
||||
* Returns string3 of input following this pattern: {string1:string2,string3}
|
||||
* @returns string
|
||||
*/
|
||||
export function getLinkDisplayTextFromCopy(copy) {
|
||||
// sample input: {routerLink:estimate,provide your VIN}
|
||||
// first split would return 'estimate,provide your VIN'
|
||||
// second split would return 'provide your VIN'
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<buttonQuestion
|
||||
class="address-vehicles-question"
|
||||
buttonType="listButton"
|
||||
buttonTypeString="listButton"
|
||||
groupName="ChooseAddressVehicle"
|
||||
:questionText="questionText"
|
||||
:answers="vehicles"
|
||||
|
|
@ -63,9 +63,7 @@ export default {
|
|||
},
|
||||
selectedVehicle() {
|
||||
// this computed is only needed for the computed differentVehicleAlertBody text above
|
||||
return this.vehicles.find(
|
||||
({ vin }) => vin === this.selectedVehicleVin[this.selectedVehicleVin.length - 1]
|
||||
);
|
||||
return this.vehicles.find(({ vin }) => vin === this.selectedVehicleVin);
|
||||
},
|
||||
},
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -26,10 +26,10 @@
|
|||
<span v-if="doesCopyContainRouterLink(copy)" class="text-body">
|
||||
<router-link
|
||||
:to="{
|
||||
query: { fmgPage: `${getRouterLinkRouteFromCopy(copy)}` },
|
||||
query: { fmgPage: `${getLinkTargetFromCopy(copy)}` },
|
||||
name: 'root',
|
||||
}"
|
||||
>{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link
|
||||
>{{ getLinkDisplayTextFromCopy(copy) }}</router-link
|
||||
>
|
||||
</span>
|
||||
<span v-else class="m-0 text-body" v-html="copy"></span>
|
||||
|
|
@ -68,8 +68,8 @@ import { isGlassAvailableForCarId } from "@/helpers/damage-helper";
|
|||
import {
|
||||
doesCopyContainRouterLink,
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
getRouterLinkRouteFromCopy,
|
||||
getRouterLinkDisplayTextFromCopy,
|
||||
getLinkTargetFromCopy,
|
||||
getLinkDisplayTextFromCopy,
|
||||
} from "@/helpers/cms-content-helper";
|
||||
import { routerParams } from "@/router/router-constants/router-params";
|
||||
import vinPagesMixin from "@/mixins/vin-pages-mixin";
|
||||
|
|
@ -153,8 +153,8 @@ export default {
|
|||
methods: {
|
||||
doesCopyContainRouterLink,
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
getRouterLinkRouteFromCopy,
|
||||
getRouterLinkDisplayTextFromCopy,
|
||||
getLinkTargetFromCopy,
|
||||
getLinkDisplayTextFromCopy,
|
||||
arePagePrerequisitesValid() {
|
||||
if (
|
||||
store.getters.order.vehicle.carId &&
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
cmsWidgetName="VinLookupMethod"
|
||||
:answers="answersFromCms"
|
||||
groupName="vinLookupMethodOption"
|
||||
buttonType="listButton"
|
||||
buttonTypeString="listButton"
|
||||
v-model="selectedVinLookupMethod"
|
||||
isRequired
|
||||
validationRules="option-required" />
|
||||
|
|
@ -174,6 +174,7 @@ export default {
|
|||
async forwardButtonAction() {
|
||||
if (this.isRepair) {
|
||||
const zipCodeData = await this.getZipCodeData(this.serviceZipCode);
|
||||
//todo: validation
|
||||
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.emailAddress, false);
|
||||
await this.dispatchStoreAction(
|
||||
storeActions.SAVE_SERVICE_LOCATION,
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
<buttonQuestion
|
||||
:answers="answersFromCms"
|
||||
:groupName="groupName"
|
||||
buttonType="listButtonHorizontal"
|
||||
buttonTypeString="listButtonHorizontal"
|
||||
v-model="selectedValues"
|
||||
isCashOrInsurance
|
||||
additionalButtonStyling="listButtonHorizontalStrong"
|
||||
isRequired />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -17,72 +17,23 @@
|
|||
cmsWidgetName="CashOrInsuranceQuestionWidget"
|
||||
v-model="isInsuranceSelected"
|
||||
groupName="CashOrInsuranceQuestion" />
|
||||
<radioServicePackage />
|
||||
<textBlock
|
||||
cmsWidgetName="quoteDisclaimer"
|
||||
justifyText="left"
|
||||
typeStyle="caption"
|
||||
class="mt-4" />
|
||||
<h3 style="m-0">h1</h3>
|
||||
<textBlock
|
||||
cmsWidgetName="quoteDisclaimer"
|
||||
justifyText="left"
|
||||
typeStyle="h1"
|
||||
class="mt-4" />
|
||||
<h3 style="m-0">h2</h3>
|
||||
<textBlock
|
||||
cmsWidgetName="quoteDisclaimer"
|
||||
justifyText="left"
|
||||
typeStyle="h2"
|
||||
class="mt-4" />
|
||||
<h3 style="m-0">h3</h3>
|
||||
<textBlock
|
||||
cmsWidgetName="quoteDisclaimer"
|
||||
justifyText="left"
|
||||
typeStyle="h3"
|
||||
class="mt-4" />
|
||||
<h3 style="m-0">h4</h3>
|
||||
<textBlock
|
||||
cmsWidgetName="quoteDisclaimer"
|
||||
justifyText="left"
|
||||
typeStyle="h4"
|
||||
class="mt-4" />
|
||||
<h3 style="m-0">h5</h3>
|
||||
<textBlock
|
||||
cmsWidgetName="quoteDisclaimer"
|
||||
justifyText="left"
|
||||
typeStyle="h5"
|
||||
class="mt-4" />
|
||||
<h3 style="m-0">h6</h3>
|
||||
<textBlock
|
||||
cmsWidgetName="quoteDisclaimer"
|
||||
justifyText="left"
|
||||
typeStyle="h6"
|
||||
class="mt-4" />
|
||||
<h3 style="m-0">Body</h3>
|
||||
<textBlock
|
||||
cmsWidgetName="quoteDisclaimer"
|
||||
justifyText="left"
|
||||
typeStyle="body"
|
||||
class="mt-4" />
|
||||
<h3 style="m-0">Label</h3>
|
||||
<textBlock
|
||||
cmsWidgetName="quoteDisclaimer"
|
||||
justifyText="left"
|
||||
typeStyle="label"
|
||||
class="mt-4" />
|
||||
<h3 style="m-0">Small</h3>
|
||||
<textBlock
|
||||
cmsWidgetName="quoteDisclaimer"
|
||||
justifyText="left"
|
||||
typeStyle="small"
|
||||
class="mt-4" />
|
||||
<h3 style="m-0">Caption</h3>
|
||||
<servicePackageQuestion
|
||||
ref="servicePackage"
|
||||
cashCmsWidgetName="CashServicePackageQuestionWidget"
|
||||
insuranceCmsWidgetName="InsuranceServicePackageQuestionWidget"
|
||||
v-model="selectedPackage"
|
||||
groupName="ServicePackageQuestion"
|
||||
:availableLineItems="availableLineItems"
|
||||
:isInsuranceSelected="isInsuranceSelected" />
|
||||
<textBlock
|
||||
cmsWidgetName="quoteDisclaimer"
|
||||
justifyText="left"
|
||||
typeStyle="caption"
|
||||
class="mt-4" />
|
||||
<modal cmsWidgetName="EstimateRainDefenseModal" />
|
||||
<modal cmsWidgetName="EstimateFrontWiperModal" />
|
||||
<modal cmsWidgetName="EstimateRearWiperModal" />
|
||||
<modal cmsWidgetName="EstimateRecalModal" />
|
||||
<funnel-footer
|
||||
cmsWidgetName="FunnelFooterWidget"
|
||||
:isForwardActionDisabled="!meta.valid"
|
||||
|
|
@ -100,10 +51,11 @@ 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 radioServicePackage from "@/ux-components/radio-service-package/radio-service-package";
|
||||
import cashOrInsuranceQuestion from "./cash-or-insurance-question/cash-or-insurance-question";
|
||||
import servicePackageQuestion from "./service-package-question/service-package-question";
|
||||
import textBlock from "@/common-components/text-block/text-block";
|
||||
import store from "@/store";
|
||||
import modal from "@/common-components/modal/modal";
|
||||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
|
||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
import { Form } from "vee-validate";
|
||||
|
|
@ -116,9 +68,9 @@ export default {
|
|||
|
||||
// TODOS - modify as needed, just a rough sketch to place initializations
|
||||
|
||||
// get supporting lineitems
|
||||
// get supporting availableLineItems
|
||||
|
||||
// get wiper and rain defense lineitems
|
||||
// get wiper and rain defense availableLineItems
|
||||
|
||||
// Settle promises and get results
|
||||
|
||||
|
|
@ -129,13 +81,63 @@ export default {
|
|||
// Call the "next" function to complete the transition to this page.
|
||||
next((vm) => {
|
||||
vm.setCmsContent(cmsContent);
|
||||
vm.isInsuranceSelected = vm.getDefaultisInsuranceSelectedValue();
|
||||
vm.availableLineItems = [
|
||||
{
|
||||
partNumber: "001",
|
||||
Description: "Windshield with Recal",
|
||||
partType: "Windshield",
|
||||
Quantity: "1",
|
||||
BasePartNumber: "001",
|
||||
Color: "Green",
|
||||
CanSafeliteRecalibrate: true,
|
||||
price: 350.22,
|
||||
},
|
||||
{
|
||||
partNumber: "SBB16",
|
||||
description: "SAFELITE BEAM BLADE 16",
|
||||
partType: "FRONT WIPER",
|
||||
price: 32.64,
|
||||
},
|
||||
{
|
||||
partNumber: "A DISPOSAL FEE",
|
||||
partType: "DISPOSAL FEE",
|
||||
price: 15.0,
|
||||
},
|
||||
{
|
||||
partNumber: "SBB26",
|
||||
description: "SAFELITE BEAM BLADE 26",
|
||||
partType: "FRONT WIPER",
|
||||
price: 53.04,
|
||||
},
|
||||
{
|
||||
partNumber: "SBBR12A",
|
||||
description: "SAFELITE REAR BLADE 12A",
|
||||
partType: "REAR WIPER",
|
||||
price: 24.48,
|
||||
},
|
||||
{
|
||||
partNumber: "RAIN DEFENSE",
|
||||
description: null,
|
||||
partType: "RAIN DEFENSE",
|
||||
price: 35.5,
|
||||
},
|
||||
{
|
||||
partNumber: "RECAL STATIC",
|
||||
Description: "Recalibration",
|
||||
partType: "recalibration",
|
||||
Quantity: "1",
|
||||
price: 150.0,
|
||||
},
|
||||
];
|
||||
vm.isInsuranceSelected = vm.getDefaultIsInsuranceSelectedValue();
|
||||
});
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pricingRequestResult: null,
|
||||
isInsurance: null,
|
||||
isInsuranceSelected: null,
|
||||
selectedPackage: null,
|
||||
availableLineItems: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -143,38 +145,27 @@ export default {
|
|||
return true;
|
||||
//return store.getters.order.damage.isRepair || (store.getters.order.lineItems?.glassParts != null && store.getters.order.lineItems.glassParts.length > 0);
|
||||
},
|
||||
getDefaultisInsuranceSelectedValue() {
|
||||
var defaultisInsuranceSelectedValue = this.$store.getters.order.payment.isInsurance;
|
||||
if (defaultisInsuranceSelectedValue != null) {
|
||||
return defaultisInsuranceSelectedValue;
|
||||
getDefaultIsInsuranceSelectedValue() {
|
||||
const defaultIsInsuranceSelectedValue = this.$store.getters.order.payment.isInsurance;
|
||||
if (defaultIsInsuranceSelectedValue != null) {
|
||||
return defaultIsInsuranceSelectedValue;
|
||||
} else {
|
||||
return this.economyPackagePrice > 600;
|
||||
return this.availableLineItems
|
||||
? baseMixin.methods.getTierOnePackagePrice(this.availableLineItems) > 500
|
||||
: null;
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
economyPackagePrice() {
|
||||
//TODO build out the pricing logic CSR-504
|
||||
return 0;
|
||||
},
|
||||
selectedChipCountValues: {
|
||||
get: function () {
|
||||
return this.modelValue;
|
||||
},
|
||||
set: function (newValue) {
|
||||
this.$emit("update:modelValue", newValue);
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
funnelHeader,
|
||||
funnelFooter,
|
||||
vehicleBanner,
|
||||
funnelSubHeader,
|
||||
Form,
|
||||
radioServicePackage,
|
||||
textBlock,
|
||||
cashOrInsuranceQuestion,
|
||||
servicePackageQuestion,
|
||||
modal,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,244 @@
|
|||
<template>
|
||||
<buttonQuestion
|
||||
:answers="servicePackageAnswers"
|
||||
:groupName="groupName"
|
||||
buttonTypeString="servicePackageRadio"
|
||||
:buttonTypeObject="servicePackageRadio"
|
||||
v-model="selectedValues"
|
||||
isRequired />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import buttonQuestion from "@/common-components/button-question/button-question";
|
||||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
import { processIfStatements } from "@/helpers/cms-content-helper";
|
||||
import { damageLocationsSelected as glassLocations } from "@/constants/damage-locations-selected";
|
||||
import servicePackageRadio from "./service-package-radio/service-package-radio";
|
||||
import { partTypeStrings } from "@/constants/part-type-strings";
|
||||
|
||||
const packageNames = {
|
||||
TIER_ONE: "TierOne",
|
||||
TIER_TWO: "TierTwo",
|
||||
TIER_THREE: "TierThree",
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "servicePackageQuestion",
|
||||
props: {
|
||||
modelValue: String,
|
||||
groupName: String,
|
||||
cashCmsWidgetName: String,
|
||||
insuranceCmsWidgetName: String,
|
||||
isInsuranceSelected: Boolean,
|
||||
availableLineItems: null,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
servicePackageRadio: servicePackageRadio,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
selectedValues: {
|
||||
get: function () {
|
||||
return this.modelValue;
|
||||
},
|
||||
set: function (newValue) {
|
||||
this.$emit("update:modelValue", newValue);
|
||||
},
|
||||
},
|
||||
nullSafeAvailableLineItems() {
|
||||
return this.availableLineItems ?? [];
|
||||
},
|
||||
servicePackageAnswers() {
|
||||
const cmsWidgetName = this.isInsuranceSelected
|
||||
? this.insuranceCmsWidgetName
|
||||
: this.cashCmsWidgetName;
|
||||
let cmsAnswersContent = this.getCmsContent(cmsWidgetName, "Answers");
|
||||
if (!cmsAnswersContent) {
|
||||
return null;
|
||||
}
|
||||
if (!this.shouldDisplayTierTwoPackage) {
|
||||
cmsAnswersContent = cmsAnswersContent.filter(
|
||||
(answer) => answer.Name != packageNames.TIER_TWO
|
||||
);
|
||||
}
|
||||
const modifiedAnswers = cmsAnswersContent.map((answer) => ({
|
||||
value: answer.Name,
|
||||
buttonLabel: this.getHeaderTextFromCms(answer.SubWidgetName),
|
||||
buttonLabelSubCopy: this.getSubheaderTextFromCms(answer.SubWidgetName),
|
||||
buttonBodyCopy: this.getBodyTextFromCms(answer.SubWidgetName),
|
||||
buttonAuxillaryCopy: this.getPackagePriceString(answer.Name),
|
||||
buttonFooterCopy: this.getFooterTextFromCms(answer.SubWidgetName),
|
||||
}));
|
||||
return modifiedAnswers;
|
||||
},
|
||||
isRecalibrationOnOrder() {
|
||||
return this.lineItemsContainsPartType(partTypeStrings.RECALIBRATION);
|
||||
},
|
||||
frontWipersApplicableForTierTwo() {
|
||||
const frontWipersAreAvailable = this.lineItemsContainsPartType(
|
||||
partTypeStrings.FRONT_WIPER
|
||||
);
|
||||
const isRepair = this.$store.getters.order.damage.isRepair;
|
||||
const glassToReplaceContainsWindshield = this.glassToReplaceContainsGlassLocation(
|
||||
glassLocations.WINDSHIELD
|
||||
);
|
||||
if (frontWipersAreAvailable) {
|
||||
if (isRepair) {
|
||||
return true;
|
||||
} else {
|
||||
if (glassToReplaceContainsWindshield) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
rearWiperApplicableForTierTwo() {
|
||||
const rearWiperIsAvailable = this.lineItemsContainsPartType(partTypeStrings.REAR_WIPER);
|
||||
return (
|
||||
this.glassToReplaceContainsGlassLocation(glassLocations.REAR) &&
|
||||
rearWiperIsAvailable
|
||||
);
|
||||
},
|
||||
frontWipersApplicableForTierThree() {
|
||||
const frontWipersAreAvailable = this.lineItemsContainsPartType(
|
||||
partTypeStrings.FRONT_WIPER
|
||||
);
|
||||
return frontWipersAreAvailable;
|
||||
},
|
||||
rearWiperApplicableForTierThree() {
|
||||
const rearWiperIsAvailable = this.lineItemsContainsPartType(partTypeStrings.REAR_WIPER);
|
||||
return (
|
||||
this.glassToReplaceContainsGlassLocation(glassLocations.REAR) &&
|
||||
rearWiperIsAvailable
|
||||
);
|
||||
},
|
||||
rainDefenseApplicableForTierThree() {
|
||||
const frontWipersAreAvailable = this.lineItemsContainsPartType(
|
||||
partTypeStrings.FRONT_WIPER
|
||||
);
|
||||
const glassToReplaceContainsWindshield = this.glassToReplaceContainsGlassLocation(
|
||||
glassLocations.WINDSHIELD
|
||||
);
|
||||
const glassToReplaceContainsRearGlass = this.glassToReplaceContainsGlassLocation(
|
||||
glassLocations.REAR
|
||||
);
|
||||
if (this.frontWipersApplicableForTierTwo) {
|
||||
return true;
|
||||
} else if (!frontWipersAreAvailable) {
|
||||
return true;
|
||||
} else if (!glassToReplaceContainsWindshield && !glassToReplaceContainsRearGlass) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
shouldDisplayTierTwoPackage() {
|
||||
return this.frontWipersApplicableForTierTwo || this.rearWiperApplicableForTierTwo;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
processIfStatements,
|
||||
getHeaderTextFromCms(cmsWidgetName) {
|
||||
return this.getCmsContent(cmsWidgetName, "HeaderText");
|
||||
},
|
||||
getSubheaderTextFromCms(cmsWidgetName) {
|
||||
return this.getCmsContent(cmsWidgetName, "SubheaderText");
|
||||
},
|
||||
getBodyTextFromCms(cmsWidgetName) {
|
||||
const bodyText = this.getCmsContent(cmsWidgetName, "BodyText");
|
||||
return this.processIfStatements(bodyText, "custom", this.getCustomValueFromString);
|
||||
},
|
||||
getFooterTextFromCms(cmsWidgetName) {
|
||||
const footerText = this.getCmsContent(cmsWidgetName, "FooterText");
|
||||
return this.processIfStatements(footerText, "custom", this.getCustomValueFromString);
|
||||
},
|
||||
getPackagePriceString(packageName) {
|
||||
const formattedPriceFloat = parseFloat(this.getPackagePrice(packageName)).toFixed(2);
|
||||
return (this.isInsuranceSelected ? "As little as $" : "$") + formattedPriceFloat;
|
||||
},
|
||||
getPackagePrice(packageName) {
|
||||
let priceFloat = this.isInsuranceSelected
|
||||
? 0
|
||||
: baseMixin.methods.getTierOnePackagePrice(this.nullSafeAvailableLineItems);
|
||||
if (packageName === packageNames.TIER_TWO) {
|
||||
priceFloat += this.getTierTwoPackageVapsPrice();
|
||||
} else if (packageName === packageNames.TIER_THREE) {
|
||||
priceFloat += this.getTierThreePackageVapsPrice();
|
||||
}
|
||||
return priceFloat;
|
||||
},
|
||||
getTierTwoPackageVapsPrice() {
|
||||
let vapsPrice = 0;
|
||||
const priceFrontWipers = this.frontWipersApplicableForTierTwo;
|
||||
const priceRearWipers = this.rearWiperApplicableForTierTwo;
|
||||
this.nullSafeAvailableLineItems.forEach((item) => {
|
||||
if (
|
||||
(priceFrontWipers &&
|
||||
item.partType.toUpperCase() === partTypeStrings.FRONT_WIPER) ||
|
||||
(priceRearWipers && item.partType.toUpperCase() === partTypeStrings.REAR_WIPER)
|
||||
) {
|
||||
vapsPrice += item.price;
|
||||
}
|
||||
});
|
||||
return vapsPrice;
|
||||
},
|
||||
getTierThreePackageVapsPrice() {
|
||||
let vapsPrice = 0;
|
||||
const priceFrontWipers = this.frontWipersApplicableForTierThree;
|
||||
const priceRearWipers = this.rearWiperApplicableForTierThree;
|
||||
const priceRainDefense = this.rainDefenseApplicableForTierThree;
|
||||
this.nullSafeAvailableLineItems.forEach((item) => {
|
||||
if (
|
||||
(priceFrontWipers &&
|
||||
item.partType.toUpperCase() === partTypeStrings.FRONT_WIPER) ||
|
||||
(priceRearWipers &&
|
||||
item.partType.toUpperCase() === partTypeStrings.REAR_WIPER) ||
|
||||
(priceRainDefense &&
|
||||
item.partType.toUpperCase() === partTypeStrings.RAIN_DEFENSE)
|
||||
) {
|
||||
vapsPrice += item.price;
|
||||
}
|
||||
});
|
||||
return vapsPrice;
|
||||
},
|
||||
getCustomValueFromString(str) {
|
||||
switch (str) {
|
||||
case "isRecalibrationOnOrder":
|
||||
return this.isRecalibrationOnOrder;
|
||||
case "frontWipersApplicableForTierTwo":
|
||||
return this.frontWipersApplicableForTierTwo;
|
||||
case "rearWiperApplicableForTierTwo":
|
||||
return this.rearWiperApplicableForTierTwo;
|
||||
case "frontWipersApplicableForTierThree":
|
||||
return this.frontWipersApplicableForTierThree;
|
||||
case "rearWiperApplicableForTierThree":
|
||||
return this.rearWiperApplicableForTierThree;
|
||||
case "rainDefenseApplicableForTierThree":
|
||||
return this.rainDefenseApplicableForTierThree;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
lineItemsContainsPartType(partType) {
|
||||
const partTypeMatches = this.nullSafeAvailableLineItems.filter(
|
||||
(lineItem) => lineItem.partType.toUpperCase() === partType
|
||||
);
|
||||
return !!partTypeMatches.length;
|
||||
},
|
||||
glassToReplaceContainsGlassLocation(glassLocation) {
|
||||
const glassLocationMatches = this.$store.getters.order.damage.glassToReplace.filter(
|
||||
(glassToReplace) => glassToReplace.glassLocation === glassLocation
|
||||
);
|
||||
return !!glassLocationMatches.length;
|
||||
},
|
||||
},
|
||||
components: {
|
||||
buttonQuestion,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
<template>
|
||||
<baseInputButton v-bind="$props" @buttonClicked="handleAnswerChange">
|
||||
<div class="package-label" for="testradio">
|
||||
<div class="package-specs">
|
||||
<p :class="[this.buttonLabelSubCopy ? 'mb-2' : 'm-0']">
|
||||
<span v-html="this.buttonLabel"></span>
|
||||
<span class="pricing-info" v-html="this.buttonAuxillaryCopy"></span>
|
||||
</p>
|
||||
<p
|
||||
class="sub-label m-0"
|
||||
v-if="this.buttonLabelSubCopy"
|
||||
v-html="this.buttonLabelSubCopy"></p>
|
||||
<!-- Parse the body text containing <ul> with logic -->
|
||||
<ul>
|
||||
<li v-for="listItem in this.arrayOfListItemsFromBodyText" :key="listItem">
|
||||
<!-- no textLink -->
|
||||
<span v-if="!doesCopyContainTextLink(listItem)" v-html="listItem"></span>
|
||||
<!-- with textLink -->
|
||||
<template
|
||||
v-else
|
||||
v-for="copy in splitCopyOnCMSPlaceHolder(listItem)"
|
||||
:key="copy">
|
||||
<span v-if="!doesCopyContainTextLink(copy)" v-html="copy"></span>
|
||||
<span v-else>
|
||||
<textLink
|
||||
linkType="text"
|
||||
:text="getLinkDisplayTextFromCopy(copy)"
|
||||
href="#!"
|
||||
data-bs-toggle="modal"
|
||||
:data-bs-target="'#' + getLinkTargetFromCopy(copy)"
|
||||
aria-label="Modal window" />
|
||||
</span>
|
||||
</template>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- End of body text parsing -->
|
||||
<div
|
||||
class="package-footer"
|
||||
v-if="this.buttonFooterCopy"
|
||||
v-html="this.buttonFooterCopy"></div>
|
||||
</div>
|
||||
</div>
|
||||
</baseInputButton>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import baseInputButton from "@/common-components/base-input-button/base-input-button";
|
||||
import textLink from "@/ux-components/text-link/text-link";
|
||||
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
|
||||
import {
|
||||
getLinkDisplayTextFromCopy,
|
||||
getLinkTargetFromCopy,
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
doesCopyContainTextLink,
|
||||
} from "@/helpers/cms-content-helper";
|
||||
|
||||
export default {
|
||||
name: "servicePackageRadio",
|
||||
mixins: [inputButtonWrapperMixin],
|
||||
components: {
|
||||
baseInputButton,
|
||||
textLink,
|
||||
},
|
||||
computed: {
|
||||
arrayOfListItemsFromBodyText() {
|
||||
return this.getArrayOfListItemsFromRawCmsCopy(this.buttonBodyCopy);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getLinkDisplayTextFromCopy,
|
||||
getLinkTargetFromCopy,
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
doesCopyContainTextLink,
|
||||
stripUlTagFromCopy(copy) {
|
||||
const regex = /(?:<ul(?:.*?)>)|(?:<\/ul>)/g;
|
||||
return copy.replace(regex, "");
|
||||
},
|
||||
getArrayOfListItemsFromRawCmsCopy(copy) {
|
||||
const withoutUlTags = this.stripUlTagFromCopy(copy);
|
||||
return withoutUlTags
|
||||
.split(/(?:<li(?:.*?)>)|(?:<\/li>)/g)
|
||||
.filter((lineItem) => lineItem);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.package-main {
|
||||
.package-wrapper {
|
||||
margin: 0.5rem 0;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
|
||||
+ .package-label {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
border: 1px solid $gray-300;
|
||||
box-shadow: 0px 4px 8px -4px rgba(0, 0, 0, 0.15),
|
||||
0px 4px 24px -8px rgba(0, 0, 0, 0.2);
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
min-height: 58px;
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
position: relative;
|
||||
top: 5px;
|
||||
margin-right: 1rem;
|
||||
border-radius: 50%;
|
||||
border: 1px solid $gray-500;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
min-width: 16px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 19px;
|
||||
top: 24px;
|
||||
border-radius: 50%;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
min-width: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
+ .package-label {
|
||||
&:before {
|
||||
border: 1px solid #8e9292;
|
||||
box-shadow: 0px 0px 0px 4px #9fcee6, 0px 1px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:checked {
|
||||
+ .package-label {
|
||||
.package-specs {
|
||||
max-height: 1000px;
|
||||
}
|
||||
}
|
||||
+ .package-label {
|
||||
background-color: $blue-100;
|
||||
border: 1px solid $blue;
|
||||
max-height: 500px;
|
||||
}
|
||||
+ .package-label {
|
||||
&:before {
|
||||
box-shadow: 0px 0px 0px 1px $blue;
|
||||
}
|
||||
}
|
||||
+ .package-label {
|
||||
&:after {
|
||||
background: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
+ .package-label {
|
||||
&:before {
|
||||
border: 2px solid $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.package-footer {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #d4281c;
|
||||
margin-left: -32px;
|
||||
}
|
||||
|
||||
.package-specs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-height: 0;
|
||||
transition: all 0.75s ease;
|
||||
|
||||
p {
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
span.pricing-info {
|
||||
color: $green;
|
||||
}
|
||||
&.sub-label {
|
||||
color: $green;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
ul {
|
||||
margin: 1rem 0 0 -15px;
|
||||
padding: 0;
|
||||
li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
isMultiSelect
|
||||
:answers="answersToDisplay"
|
||||
:groupName="groupName"
|
||||
buttonType="listCard"
|
||||
buttonTypeString="listCard"
|
||||
isRequired
|
||||
v-model="selectedValues"
|
||||
validationRules="damage-location-required" />
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
:isMultiSelect="isMultiSelect"
|
||||
:answers="answersToDisplay"
|
||||
:groupName="groupName"
|
||||
buttonType="listCard"
|
||||
buttonTypeString="listCard"
|
||||
v-model="selectedValues"
|
||||
:validationRules="validationRules"
|
||||
:suppressError="suppressError"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
isMultiSelect
|
||||
:answers="answersToDisplay"
|
||||
:groupName="groupName"
|
||||
buttonType="listCard"
|
||||
buttonTypeString="listCard"
|
||||
v-model="selectedDoorSidesValues"
|
||||
validationRules="damage-side-required"
|
||||
isRequired />
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
:questionText="questionText"
|
||||
:answers="answersFromCms"
|
||||
:groupName="groupName"
|
||||
buttonType="listButtonHorizontal"
|
||||
buttonTypeString="listButtonHorizontal"
|
||||
useTextForValue
|
||||
v-model="selectedValue"
|
||||
:validationRules="validationRules"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
:questionText="questionText"
|
||||
:answers="answersFromCms"
|
||||
:groupName="groupName"
|
||||
buttonType="listCard"
|
||||
buttonTypeString="listCard"
|
||||
v-model="selectedValues"
|
||||
:suppressError="suppressError"
|
||||
:validationRules="validationRules"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
<buttonQuestion
|
||||
v-model="selectedTint"
|
||||
:answers="tintSelectionOptions"
|
||||
buttonType="listCard"
|
||||
buttonTypeString="listCard"
|
||||
:isWide="true"
|
||||
altText=""
|
||||
isRequired
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
<div class="col">
|
||||
<buttonQuestion
|
||||
v-model="selectedPartNumber"
|
||||
buttonType="radio"
|
||||
buttonTypeString="radio"
|
||||
class="radioQuestion"
|
||||
:questionText="glassFeatureQuestion"
|
||||
:answers="featureListData[selectedTint]"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { vehicleCategories } from "@/constants/vehicle-categories.js";
|
|||
import { routerParams } from "@/router/router-constants/router-params";
|
||||
import { queryStrings } from "@/constants/query-strings";
|
||||
import { dynamicStrings } from "@/constants/dynamic-strings";
|
||||
import { partTypeStrings } from "../constants/part-type-strings";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
|
|
@ -61,6 +62,20 @@ export default {
|
|||
state: serviceZipValidationResponse.data.state,
|
||||
};
|
||||
},
|
||||
getTierOnePackagePrice(lineItems) {
|
||||
let totalPrice = 0;
|
||||
lineItems.forEach((lineItem) => {
|
||||
const partType = lineItem.partType.toUpperCase();
|
||||
if (
|
||||
partType != partTypeStrings.FRONT_WIPER &&
|
||||
partType != partTypeStrings.REAR_WIPER &&
|
||||
partType != partTypeStrings.RAIN_DEFENSE
|
||||
) {
|
||||
totalPrice += lineItem.price;
|
||||
}
|
||||
});
|
||||
return totalPrice;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
storeActions() {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ export default {
|
|||
...inputButtonProps,
|
||||
buttonLabel: [Number, String],
|
||||
buttonLabelSubCopy: String,
|
||||
buttonBodyCopy: String,
|
||||
buttonAuxillaryCopy: String,
|
||||
buttonFooterCopy: String,
|
||||
buttonImage: String,
|
||||
buttonImageId: String,
|
||||
altText: {
|
||||
|
|
@ -18,6 +21,7 @@ export default {
|
|||
textPosition: String,
|
||||
screenReaderOnlyText: String,
|
||||
isWide: Boolean,
|
||||
additionalButtonStyling: String,
|
||||
},
|
||||
computed: {
|
||||
selectedValue: {
|
||||
|
|
|
|||
|
|
@ -392,6 +392,12 @@ export const getters = {
|
|||
},
|
||||
eventBus: (state) => state.applicationUser.eventBus,
|
||||
damage: (state) => state.order.damage,
|
||||
hasAnyNonWindshieldGlassParts: (state) => {
|
||||
const nonWindshieldItems = state.order.damage.glassToReplace.filter(
|
||||
(glassToReplace) => glassToReplace.glassLocation != "Windshield"
|
||||
);
|
||||
return !!nonWindshieldItems.length;
|
||||
},
|
||||
lineItems: (state) => state.order.lineItems,
|
||||
pageData: (state) => (page) => {
|
||||
return state.applicationUser.pageData[page];
|
||||
|
|
|
|||
|
|
@ -1,9 +1,37 @@
|
|||
html {
|
||||
.has-error {
|
||||
// START HOVER
|
||||
&.list-button-horizontal,
|
||||
&.list-button,
|
||||
&.list-card,
|
||||
&.list-card.list-button {
|
||||
&.list-card {
|
||||
border: 1px solid $red;
|
||||
position: relative;
|
||||
z-index: 4;
|
||||
|
||||
.button-content {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@include box-shadow-hover($red-200);
|
||||
z-index: 5;
|
||||
}
|
||||
}
|
||||
|
||||
&.ui-radio {
|
||||
&:hover {
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
@include box-shadow-hover($red-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
// END HOVER
|
||||
|
||||
&.list-button,
|
||||
&.list-card {
|
||||
color: $red;
|
||||
|
||||
input[type="checkbox"]:focus + label,
|
||||
input[type="radio"]:focus + label {
|
||||
box-shadow: 0 0 0 2.5px $red;
|
||||
|
|
@ -12,32 +40,17 @@ html {
|
|||
box-shadow: 0 0 0 1px $red;
|
||||
}
|
||||
&:hover {
|
||||
box-shadow: 0px 0px 0px 4px $red-200;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
label {
|
||||
border: 1px solid $red;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
label:hover {
|
||||
box-shadow: 0px 0px 0px 4px $red-200;
|
||||
border-radius: 10px;
|
||||
border: 1px solid $red;
|
||||
}
|
||||
}
|
||||
&.list-button-horizontal {
|
||||
color: $red;
|
||||
label {
|
||||
border: 1px solid $red;
|
||||
&:hover {
|
||||
box-shadow: 0px 0px 0px 4px $red-200;
|
||||
}
|
||||
}
|
||||
input[type="checkbox"]:focus + label,
|
||||
input[type="radio"]:focus + label {
|
||||
box-shadow: 0 0 1px $red;
|
||||
}
|
||||
}
|
||||
|
||||
&.grid-item {
|
||||
input[type="radio"] {
|
||||
+ label {
|
||||
|
|
@ -64,23 +77,21 @@ html {
|
|||
}
|
||||
&.textbox-question,
|
||||
&.dropdown-question {
|
||||
input:hover {
|
||||
box-shadow: 0px 0px 0px 4px $red-200;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid $red;
|
||||
}
|
||||
input:focus {
|
||||
box-shadow: 0 0 0 2.5px $red;
|
||||
}
|
||||
p {
|
||||
color: $red;
|
||||
}
|
||||
input,
|
||||
select {
|
||||
border: 1px solid transparent;
|
||||
box-shadow: 0 0 0 1px $red;
|
||||
border: 1px solid $red;
|
||||
&:focus {
|
||||
border: 1px solid transparent;
|
||||
box-shadow: 0 0 0 2.5px $red;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0px 0px 0px 4px $red-200;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid $red;
|
||||
}
|
||||
}
|
||||
select {
|
||||
|
|
|
|||
|
|
@ -4,3 +4,7 @@
|
|||
@mixin blue-gradient {
|
||||
background: linear-gradient(270deg, $blue 0%, $blue-800 100%);
|
||||
}
|
||||
|
||||
@mixin box-shadow-hover($color) {
|
||||
box-shadow: 0 0 0 4px $color;
|
||||
}
|
||||
|
|
|
|||
15
src/styles/shared-input-button-styles.scss
Normal file
15
src/styles/shared-input-button-styles.scss
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
.base-input-button {
|
||||
&:not(.has-error):hover {
|
||||
cursor: pointer;
|
||||
|
||||
&.list-button,
|
||||
&.list-button-horizontal,
|
||||
&.list-card {
|
||||
&:not(.selected) {
|
||||
position: relative;
|
||||
z-index: 4;
|
||||
@include box-shadow-hover($blue-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,10 +19,10 @@
|
|||
<span v-else>
|
||||
<router-link
|
||||
:to="{
|
||||
query: { fmgPage: `${getRouterLinkRouteFromCopy(copy)}` },
|
||||
query: { fmgPage: `${getLinkTargetFromCopy(copy)}` },
|
||||
name: 'root',
|
||||
}"
|
||||
>{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link
|
||||
>{{ getLinkDisplayTextFromCopy(copy) }}</router-link
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
|
|
@ -41,8 +41,8 @@
|
|||
import {
|
||||
doesCopyContainRouterLink,
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
getRouterLinkRouteFromCopy,
|
||||
getRouterLinkDisplayTextFromCopy,
|
||||
getLinkTargetFromCopy,
|
||||
getLinkDisplayTextFromCopy,
|
||||
splitCMSCopyOnParagraphTag,
|
||||
} from "@/helpers/cms-content-helper";
|
||||
|
||||
|
|
@ -92,8 +92,8 @@ export default {
|
|||
methods: {
|
||||
doesCopyContainRouterLink,
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
getRouterLinkRouteFromCopy,
|
||||
getRouterLinkDisplayTextFromCopy,
|
||||
getLinkTargetFromCopy,
|
||||
getLinkDisplayTextFromCopy,
|
||||
ensureAlertIsInViewPort() {
|
||||
if (this.shouldScrollToOnMount && this.$el.style.display != "none") {
|
||||
var footerHeight = this.getFooterInfoBoxHeight();
|
||||
|
|
|
|||
|
|
@ -52,12 +52,12 @@ describe("list-button-horizontal.vue", () => {
|
|||
expect(input.attributes()["aria-required"]).toEqual("true");
|
||||
});
|
||||
|
||||
it("is cash or insurance button => has 'radio-fancy' class", () => {
|
||||
it("is cash or insurance button => has 'strong' class", () => {
|
||||
// Arrange/Act
|
||||
const { wrapper } = setupMocks({
|
||||
mockData: {
|
||||
propsData: {
|
||||
isCashOrInsurance: true,
|
||||
additionalButtonStyling: "listButtonHorizontalStrong",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -65,7 +65,7 @@ describe("list-button-horizontal.vue", () => {
|
|||
// Assert
|
||||
const label = wrapper.find("label");
|
||||
|
||||
expect(label.classes()).toContain("radio-fancy");
|
||||
expect(label.classes()).toContain("strong");
|
||||
});
|
||||
|
||||
test("has buttonLabel => displays buttonLabel", () => {
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@
|
|||
<baseInputButton
|
||||
v-bind="$props"
|
||||
:buttonWrapperClasses="[
|
||||
'list-group list-button-horizontal d-flex flex-column w-100',
|
||||
{ 'radio-fancy': isCashOrInsurance },
|
||||
'list-group list-button-horizontal d-flex flex-column w-100 base-input-button',
|
||||
{ strong: isStrongStyling },
|
||||
]"
|
||||
v-model="selectedValue">
|
||||
<div class="list-button-horizontal-content d-flex flex-column justify-content-center p-3">
|
||||
<div
|
||||
class="button-content list-button-horizontal-content d-flex flex-column justify-content-center p-3">
|
||||
<span class="m-0" :class="textPosition">
|
||||
{{ buttonLabel }}
|
||||
</span>
|
||||
|
|
@ -27,8 +28,10 @@ import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
|
|||
export default {
|
||||
name: "listButtonHorizontal",
|
||||
mixins: [inputButtonWrapperMixin],
|
||||
props: {
|
||||
isCashOrInsurance: Boolean,
|
||||
computed: {
|
||||
isStrongStyling() {
|
||||
return this.additionalButtonStyling === "listButtonHorizontalStrong";
|
||||
},
|
||||
},
|
||||
components: {
|
||||
baseInputButton,
|
||||
|
|
@ -85,25 +88,13 @@ export default {
|
|||
width: 100%;
|
||||
color: $gray-600;
|
||||
|
||||
&:hover {
|
||||
@include media-breakpoint-up(sm) {
|
||||
box-shadow: 0 0 0 4px $blue-300;
|
||||
cursor: pointer;
|
||||
z-index: 4 !important;
|
||||
}
|
||||
}
|
||||
|
||||
+ p {
|
||||
display: none;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Cash/Insurance option radio button styling
|
||||
&.radio-fancy {
|
||||
&.strong {
|
||||
.list-button-horizontal-content {
|
||||
border: 1px solid $blue-700;
|
||||
z-index: 2;
|
||||
|
|
@ -163,9 +154,15 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
.col {
|
||||
.col,
|
||||
.list-group {
|
||||
border-radius: 0;
|
||||
|
||||
&:first-of-type {
|
||||
.list-button-horizontal {
|
||||
border-bottom-left-radius: 0.5rem;
|
||||
border-top-left-radius: 0.5rem;
|
||||
|
||||
.list-button-horizontal-content {
|
||||
border-bottom-left-radius: 0.5rem;
|
||||
border-top-left-radius: 0.5rem;
|
||||
|
|
@ -175,6 +172,9 @@ export default {
|
|||
|
||||
&:last-of-type {
|
||||
.list-button-horizontal {
|
||||
border-bottom-right-radius: 0.5rem;
|
||||
border-top-right-radius: 0.5rem;
|
||||
|
||||
.list-button-horizontal-content {
|
||||
border-bottom-right-radius: 0.5rem;
|
||||
border-top-right-radius: 0.5rem;
|
||||
|
|
@ -184,7 +184,7 @@ export default {
|
|||
|
||||
//Cash/insurance styling
|
||||
&:first-of-type {
|
||||
.list-button-horizontal.radio-fancy {
|
||||
.list-button-horizontal.strong {
|
||||
input[type="radio"] {
|
||||
&:checked + .list-button-horizontal-content {
|
||||
border-bottom-right-radius: 0;
|
||||
|
|
@ -200,7 +200,7 @@ export default {
|
|||
}
|
||||
|
||||
&:last-of-type {
|
||||
.list-button-horizontal.radio-fancy {
|
||||
.list-button-horizontal.strong {
|
||||
input[type="radio"] {
|
||||
&:checked + .list-button-horizontal-content {
|
||||
border-bottom-left-radius: 0;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<baseInputButton
|
||||
v-bind="$props"
|
||||
buttonWrapperClasses="list-group list-button rounded-3 d-flex flex-column w-100 mb-2"
|
||||
buttonWrapperClasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 mb-2"
|
||||
v-model="selectedValue">
|
||||
<div
|
||||
:aria-label="buttonLabel"
|
||||
class="list-button-content d-flex flex-column justify-content-center py-3 px-4">
|
||||
class="button-content list-button-content d-flex flex-column justify-content-center py-3 px-4">
|
||||
<span class="m-0" :class="textPosition">
|
||||
{{ buttonLabel }}
|
||||
</span>
|
||||
|
|
@ -111,16 +111,6 @@ export default {
|
|||
color: $gray-550;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@include media-breakpoint-up(sm) {
|
||||
box-shadow: 0 0 0 4px $blue-300;
|
||||
}
|
||||
cursor: pointer;
|
||||
}
|
||||
+ p {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
<baseInputButton
|
||||
v-bind="$props"
|
||||
:buttonWrapperClasses="[
|
||||
'list-card w-100 rounded-3 d-flex align-items-center h-100',
|
||||
'list-card w-100 rounded-3 d-flex align-items-center h-100 base-input-button',
|
||||
{ horizontal: isWide },
|
||||
]"
|
||||
v-model="selectedValue">
|
||||
<div
|
||||
class="d-flex w-100 align-items-center px-2 h-100 list-card-content"
|
||||
class="d-flex w-100 align-items-center px-2 h-100 list-card-content button-content rounded-3"
|
||||
:class="labelClasses">
|
||||
<img
|
||||
:id="buttonImageId"
|
||||
|
|
@ -74,9 +74,6 @@ export default {
|
|||
border: 1px solid $gray-500;
|
||||
|
||||
&.has-error {
|
||||
//Red border if invalid
|
||||
border: 1px solid $red;
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
@include list-card-focus($red);
|
||||
|
|
@ -91,13 +88,6 @@ export default {
|
|||
max-width: 100%;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@include media-breakpoint-up(sm) {
|
||||
box-shadow: 0px 0px 0px 4px $blue-300;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
position: absolute;
|
||||
|
|
@ -107,10 +97,6 @@ export default {
|
|||
display: block;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
p {
|
||||
color: $gray-600;
|
||||
text-align: center;
|
||||
|
|
|
|||
|
|
@ -1,232 +0,0 @@
|
|||
<template>
|
||||
<div class="package-main">
|
||||
<div class="package-wrapper">
|
||||
<input type="radio" name="test-radio" id="testradio" />
|
||||
<label for="testradio">
|
||||
<div class="package-specs">
|
||||
<p class="m-0">Economy service <span>As little as $0.00</span></p>
|
||||
<ul>
|
||||
<li>New replacement windshield</li>
|
||||
<li>Expert installation</li>
|
||||
<li>Nationwide lifetime warranty</li>
|
||||
</ul>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="package-wrapper">
|
||||
<input type="radio" name="test-radio" id="testradio-2" />
|
||||
<label for="testradio-2">
|
||||
<div class="package-specs">
|
||||
<p class="mb-2">Standard service <span>As little as 59.97</span></p>
|
||||
<p class="sub-label m-0">most popular</p>
|
||||
<ul>
|
||||
<li>New replacement windshield</li>
|
||||
<li>
|
||||
<textLink
|
||||
linkType="text"
|
||||
text="Rain Defense"
|
||||
href="#!"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#EstimateRainDefenseModal"
|
||||
aria-label="Modal window" />
|
||||
treatment
|
||||
</li>
|
||||
<li>
|
||||
New
|
||||
<textLink
|
||||
linkType="text"
|
||||
text="front wiper blades"
|
||||
href="#!"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#EstimateFrontWiperModal"
|
||||
aria-label="Modal window" />
|
||||
</li>
|
||||
<li>
|
||||
New
|
||||
<textLink
|
||||
linkType="text"
|
||||
text="rear wiper blade"
|
||||
href="#!"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#EstimateRearWiperModal"
|
||||
aria-label="Modal window" />
|
||||
</li>
|
||||
<li>
|
||||
Expert
|
||||
<textLink
|
||||
linkType="text"
|
||||
text="recalibration"
|
||||
href="#!"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#EstimateRecalModal"
|
||||
aria-label="Modal window" />
|
||||
</li>
|
||||
<li>Nationwide lifetime warranty</li>
|
||||
<li>New wiper blades</li>
|
||||
</ul>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="package-wrapper">
|
||||
<input type="radio" name="test-radio" id="testradio-3" />
|
||||
<label for="testradio-3">
|
||||
<div class="package-specs">
|
||||
<p class="mb-2">Premium service <span>As little as $94.97</span></p>
|
||||
<ul>
|
||||
<li>New replacement windshield</li>
|
||||
<li>Expert installation</li>
|
||||
<li>Nationwide lifetime warranty</li>
|
||||
<li>New wiper blades</li>
|
||||
<li>Rain Defense</li>
|
||||
</ul>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<modal cmsWidgetName="EstimateRainDefenseModal" />
|
||||
<modal cmsWidgetName="EstimateFrontWiperModal" />
|
||||
<modal cmsWidgetName="EstimateRearWiperModal" />
|
||||
<modal cmsWidgetName="EstimateRecalModal" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import textLink from "@/ux-components/text-link/text-link";
|
||||
import modal from "@/common-components/modal/modal";
|
||||
export default {
|
||||
name: "radioServicePackage",
|
||||
data() {
|
||||
return {
|
||||
isActive: false,
|
||||
hasArrow: true,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleClass: function (event) {
|
||||
this.isActive = !this.isActive;
|
||||
},
|
||||
},
|
||||
components: {
|
||||
textLink,
|
||||
modal,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.package-main {
|
||||
.package-wrapper {
|
||||
margin: 0.5rem 0;
|
||||
input[type="radio"] {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
|
||||
+ label {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
border: 1px solid $gray-300;
|
||||
box-shadow: 0px 4px 8px -4px rgba(0, 0, 0, 0.15),
|
||||
0px 4px 24px -8px rgba(0, 0, 0, 0.2);
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
min-height: 58px;
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
position: relative;
|
||||
top: 5px;
|
||||
margin-right: 1rem;
|
||||
border-radius: 50%;
|
||||
border: 1px solid $gray-500;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
min-width: 16px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 19px;
|
||||
top: 24px;
|
||||
border-radius: 50%;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
min-width: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
+ label {
|
||||
&:before {
|
||||
border: 1px solid #8e9292;
|
||||
box-shadow: 0px 0px 0px 4px #9fcee6, 0px 1px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:checked {
|
||||
+ label {
|
||||
.package-specs {
|
||||
max-height: 1000px;
|
||||
}
|
||||
}
|
||||
+ label {
|
||||
background-color: $blue-100;
|
||||
border: 1px solid $blue;
|
||||
max-height: 500px;
|
||||
}
|
||||
+ label {
|
||||
&:before {
|
||||
box-shadow: 0px 0px 0px 1px $blue;
|
||||
}
|
||||
}
|
||||
+ label {
|
||||
&:after {
|
||||
background: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
+ label {
|
||||
&:before {
|
||||
border: 2px solid $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.package-specs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-height: 0;
|
||||
transition: all 0.75s ease;
|
||||
|
||||
p {
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
span {
|
||||
color: $green;
|
||||
}
|
||||
&.sub-label {
|
||||
color: $green;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
ul {
|
||||
margin: 1rem 0 0 -15px;
|
||||
padding: 0;
|
||||
li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<baseInputButton
|
||||
v-bind="$props"
|
||||
buttonWrapperClasses="ui-radio form-check"
|
||||
buttonWrapperClasses="ui-radio form-check base-input-button"
|
||||
inputClasses="form-check-input"
|
||||
v-model="selectedValue">
|
||||
<div class="d-flex align-items-start form-check-label">
|
||||
|
|
@ -64,7 +64,7 @@ export default {
|
|||
}
|
||||
|
||||
&:hover {
|
||||
.form-check-input {
|
||||
.form-check-input:not(:checked) {
|
||||
box-shadow: 0 0 0 4px $blue-300;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue