Added conditional logic for state specific steering

This commit is contained in:
Jason Wheeler 2023-04-17 16:33:09 -04:00
parent 1681d0cce7
commit 2e27b1fc44
3 changed files with 260 additions and 4 deletions

View file

@ -141,6 +141,12 @@ 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]);
}
@ -221,6 +227,192 @@ function mapStringToState(str) {
return stringBuilder.trimStart();
}
function getStoreValueFromString(str) {
let storeOrStateObject = useMainStore();
for (const s of str.split('.')) {
if (s === 'getters') continue;
if (storeOrStateObject[s] != undefined) {
storeOrStateObject = storeOrStateObject[s];
} else {
return ''
}
}
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
);
//str = str.replace(/\r?\n|\r/g, '');
const hasEmbeddedCrLf = /\r?\n|\r/g.test(str);
if (!containsRelevantIfStatement || hasEmbeddedCrLf) {
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++;
}
console.error('Did not find end in conditional logic');
return matches;
}
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 setupModalLinks(context) {
context.$nextTick(() => {
const elements = document.getElementsByClassName("modal-text")

View file

@ -74,6 +74,12 @@ const ProviderPreferenceMockData = {
return siteFooterWidgetMockData.ForwardButtonText;
}
}
if (cmsWidgetName === 'StateSpecificSteeringText') {
if (fieldName === 'BodyText') {
return "{if:custom:OH}ohioText{else}{if:custom:CA}cali's Text{end}{end}";
}
}
if (cmsWidgetName === 'ProviderPreference') {
if (fieldName === 'QuestionText') {
@ -106,7 +112,41 @@ const ProviderPreferenceMockData = {
return { mockRoute, mockRouter, wrapper };
}
describe('provider-preference.vue', () => {
test('"Continue" button is disabled when no Shop Location is selected.', () => {
test("getStateSpecificText returns true when values match", () => {
const { wrapper } = setupMocks();
useMainStore().order.customer.address.state = "ohio";
const actual = wrapper.vm.getStateSpecificText("Ohio")
expect(actual).toBeTruthy();
});
test("getStateSpecificText returns false when values do not match", () => {
const { wrapper } = setupMocks();
useMainStore().order.customer.address.state = "delaware";
const actual = wrapper.vm.getStateSpecificText("Ohio")
expect(actual).toBeFalsy();
});
test("getStateSpecificText should return correct value for state", () => {
const { wrapper } = setupMocks();
useMainStore().order.customer.address.state = "ca";
let actual = wrapper.vm.steeringModalHeader;
expect(actual).toBe("cali's Text");
useMainStore().order.customer.address.state = "oH";
actual = wrapper.vm.steeringModalHeader;
expect(actual).toBe("ohioText");
});
test('"Continue" button is disabled when no Shop Location is selected.', () => {
const { wrapper } = setupMocks();
const continueButton = wrapper.get('[data-test-id="site-footer-main-button"]');

View file

@ -10,6 +10,7 @@
<div class="col">
<div class="select-car-form rounded">
<div v-html="ProviderPreferenceHeaderText" class="text-center mb-5 modal-link" ></div>
<a modalTarget="SteeringModal" class="modal-text" href="#!" aria-label="Modal window">Wow!!</a>
<div
v-html="ProviderPreferenceBodyText"
class="mt-0 body-text"
@ -41,16 +42,24 @@
<contentGroupModal cmsWidgetName="ShopPreferenceDrawer"
ref="ShopPreferenceDrawer"
/>
<modal
ref="SteeringModal"
modalId="SteeringModal"
:footerButtonText="steeringModalFooter">
<h1>{{steeringModalHeader}}</h1>
<div>{{steeringModalBody}}</div>
</modal>
</Form>
</template>
<script>
// Import Supporting Files
import { fetchCmsContentForPage, setupModalLinks } from '@/helpers/cms-content-helper';
import { fetchCmsContentForPage, setupModalLinks, processIfStatements } from '@/helpers/cms-content-helper';
import { settleAllPromises } from "@/helpers/layout-helper";
import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
import buttonQuestion from "@/digital-components/button-question/button-question";
import contentGroupModal from "@/iss-components/content-group-modal/content-group-modal"
import contentGroupModal from "@/iss-components/content-group-modal/content-group-modal";
import modal from "@/digital-components/modal/modal.vue"
// Import Component
import baseFormMixin from "@/mixins/base-form-mixin";
@ -71,7 +80,8 @@ export default {
siteSubHeader,
Form,
buttonQuestion,
contentGroupModal
contentGroupModal,
modal
},
data() {
return {
@ -108,8 +118,22 @@ export default {
answersFromCms() {
return this.getCmsContent("ServiceLocationQuestion", "Answers");
},
steeringModalHeader() {
const bodyText = this.getCmsContent("StateSpecificSteeringText", "BodyText");
 return processIfStatements(bodyText, "custom", this.getStateSpecificText);
},
steeringModalBody() {
const bodyText = this.getCmsContent("StateSpecificSteeringText", "BodyText2");
 return processIfStatements(bodyText, "custom", this.getStateSpecificText);
},
steeringModalFooter() {
return this.getCmsContent("StateSpecificSteeringText", "FooterText");
}
},
methods: {
getStateSpecificText(value) {
return this.mainStore.order.customer.address.state?.toLowerCase() == value?.toLowerCase();
},
arePagePrerequisiteValid() {
return true;
},