diff --git a/src/helpers/cms-content-helper.js b/src/helpers/cms-content-helper.js index 656b2b5f..a4d6f7d1 100644 --- a/src/helpers/cms-content-helper.js +++ b/src/helpers/cms-content-helper.js @@ -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: ? syntax stores the captured match like so: match.groups.variableName + const matchStartOfString = + '(?^.+?)' + // Match and Capture all characters (lazy), cannot be empty + '(?=(?:{if))'; // Looks ahead but does not capture {if + const matchIfOperator = + '(?{if:)' + // Match & Capture {if: + '(?.*?):' + // Match all chars up to and including next ':' - Capture all chars up to ':' + '(?.*?)}' + // Match all chars up to and including next '}' - Capture all chars up to '}' + '(?.*?)' + // Match and Capture all characters (lazy), can be empty + '(?=' + + anyLogicOperatorNonCapture + + ')'; // Looks ahead but does not capture the next logic operator + const matchElseOperator = + '(?{else})' + // Match & Capture {else} + '(?.*?)' + // Match & Capture all characters (lazy), can be empty + '(?=' + + anyLogicOperatorNonCapture + + ')'; // Looks ahead but does not capture the next logic operator + const matchEndOperator = + '(?{end})' + // Match & Capture {end} + '(?.*?)' + // 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") diff --git a/src/layouts/provider-preference/provider-preference.spec.js b/src/layouts/provider-preference/provider-preference.spec.js index 24f7cbb2..4a752f7c 100644 --- a/src/layouts/provider-preference/provider-preference.spec.js +++ b/src/layouts/provider-preference/provider-preference.spec.js @@ -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"]'); diff --git a/src/layouts/provider-preference/provider-preference.vue b/src/layouts/provider-preference/provider-preference.vue index f4064ffa..3f8920eb 100644 --- a/src/layouts/provider-preference/provider-preference.vue +++ b/src/layouts/provider-preference/provider-preference.vue @@ -10,6 +10,7 @@
+ Wow!!
+ +

{{steeringModalHeader}}

+
{{steeringModalBody}}
+