diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 2db5a1ef..caca6e33 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -75,4 +75,8 @@ stages: clearFolder: true deployFolder: '' region: us-east-2 - cfDistributionId: $(cfDistributionId) \ No newline at end of file + appDeployVariables: + __VUE_APP_CONSUMER_CF_DISTRO__: $(__VUE_APP_CONSUMER_CF_DISTRO__) + __VUE_APP_CURRENT_ENVIRONMENT__: $(__VUE_APP_CURRENT_ENVIRONMENT__) + cfDistributionId: $(cfDistributionId) + \ No newline at end of file diff --git a/src/constants/application-config.js b/src/constants/application-config.js index ff2229e3..9c3a3d05 100644 --- a/src/constants/application-config.js +++ b/src/constants/application-config.js @@ -2,7 +2,8 @@ const applicationConfig = { CURRENT_ENVIRONMENT: process.env.VUE_APP_CURRENT_ENVIRONMENT, // "Localhost", "Dev", "QA", and "Prod" CONSUMER_CF_DISTRO: process.env.VUE_APP_CONSUMER_CF_DISTRO, APPLICATION_NAME: "SelfService", - SITE_ENTRY_TRIGGER_VALUE: "SelfService" + SITE_ENTRY_TRIGGER_VALUE: "SelfService", + APPLICATION_ABBREVIATION: "iss", }; export { applicationConfig }; \ No newline at end of file diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index 7581f022..19833112 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -1,17 +1,17 @@ const endpoints = { - GetRouteInfo: { - url: "/content/api/v1/content/RouteInfo", - method: "POST", - }, - GetHomepageInfo: { - url: "/content/api/v1/content/HomepageInfo", - method: "GET", - }, - GetPageData: { - url: "/content/api/v1/content", - method: "GET", - }, - }; + GetRouteInfo: { + url: (applicationAbbreviation) => `/content/api/v1/content/${applicationAbbreviation}/RouteInfo`, + method: "POST", + }, + GetHomepageInfo: { + url: (applicationAbbreviation) => `/content/api/v1/content/${applicationAbbreviation}/HomepageInfo`, + method: "GET", + }, + GetPageData: { + url: (applicationAbbreviation, pageName) => `/content/api/v1/content/${applicationAbbreviation}/${pageName}`, + method: "GET", + }, +}; - export { endpoints }; +export { endpoints }; \ No newline at end of file diff --git a/src/global-methods.spec.js b/src/global-methods.spec.js new file mode 100644 index 00000000..286fe976 --- /dev/null +++ b/src/global-methods.spec.js @@ -0,0 +1,77 @@ +import axios from 'axios'; +import globalMethods from "@/global-methods"; + +//Mock external dependencies +jest.mock("axios"); + +it("Global Methods - Call Http Client - Should Resolve Promise", () => { + //Arrange + const endpoint = "https://mock.safelite.com"; + const httpArgs = setupMocksForHttpClient({ endpoint: endpoint }); + + //Act + globalMethods.callHttpClient(httpArgs).then((response) => { + //Assert + expect(axios.mock.calls[0][0].url).toContain(endpoint); + expect(response.data.message).toContain("Success"); + expect(response.status).toEqual(200); + }); + }); + + it("Global Methods - Call Http Client - Should Reject Promise", () => { + //Arrange + const endpoint = "https://mock.safelite.com"; + const httpArgs = setupMocksForHttpClient({ + endpoint: endpoint, + isError: true, + }); + + //Act + globalMethods.callHttpClient(httpArgs).catch((err) => { + //Assert + expect(axios.mock.calls[0][0].url).toContain(endpoint); + expect(err.data.message).toContain("Error"); + expect(err.status).toEqual(500); + }); + }); + + function setupMocksForHttpClient({ + endpoint = null, + isError = false, + additionalData = null, + }) { + //Clear node module + axios.mockClear(); + + // Success Response + const response = { + status: 200, + data: { + message: "Success", + additionalData: additionalData, + }, + }; + + // Error Response + const error = { + response: { + status: 500, + data: { + message: "Error", + additionalData: additionalData, + }, + }, + }; + + // Error interceptor on Axios returns a different object, so we need to mimic that. + if (isError) { + axios.mockRejectedValue(error); + } else { + axios.mockResolvedValue(response); + } + + return { + endpoint: endpoint, + logApiCall: true + }; + } \ No newline at end of file diff --git a/src/helpers/cms-content-helper.js b/src/helpers/cms-content-helper.js new file mode 100644 index 00000000..5dd0c87f --- /dev/null +++ b/src/helpers/cms-content-helper.js @@ -0,0 +1,121 @@ +import { dynamicStrings } from "@/constants/dynamic-strings"; +import { useMainStore } from '@/store'; + +export function fetchCmsContentForPage(issPage) { + return useMainStore().getPageData(issPage) + .then((response) => { + const pageDataFromCms = {}; + + response.data.Result.forEach((widget) => { + let widgetWithReplacements = findAndReplaceGlobalStateValues( + widget.Model, + widget.Name + ); + + // If we already have this widget, push it on the collection + if (widgetWithReplacements.Name in pageDataFromCms) { + pageDataFromCms[widgetWithReplacements.Name].push( + widgetWithReplacements.Model + ); + return; + } + + pageDataFromCms[widgetWithReplacements.Name] = [ + widgetWithReplacements.Model, + ]; + }); + + Object.keys(pageDataFromCms).forEach((key) => { + if (pageDataFromCms[key].length === 1) { + pageDataFromCms[key] = pageDataFromCms[key][0]; + } + }); + + return pageDataFromCms; + }); + } + + // Parent function for processWidgetItemForReplacement. This will loop through the parent +// object and pass any objects that need additional processing to the processWidgetItemForReplacement function. +function findAndReplaceGlobalStateValues(widgetModel, widgetName) { + const objWithReplacements = { + Name: widgetName, + Model: {}, + }; + + Object.keys(widgetModel).forEach((key) => { + let modelWithReplacements = processWidgetItemForReplacement( + widgetModel, + key + ); + + objWithReplacements.Model[key] = modelWithReplacements; + }); + + return objWithReplacements; + } + + // This function will process the widget item and replace any global state variables with their values. +// This is a recursive function, it will call itself until it runs out of items to iterate on given the object. +function processWidgetItemForReplacement(widgetModel, key) { + // If we have a string, and it needs to be replaced. + if (typeof widgetModel[key] === "string") { + if (widgetModel[key].includes(dynamicStrings.GLOBAL_STATE)) { + widgetModel[key] = mapStringToState(widgetModel[key]); + } + return widgetModel[key]; + } + + // If we have an object. array, etc + if ( + typeof widgetModel[key] === "object" && + Object.keys(widgetModel[key]).length + ) { + Object.keys(widgetModel[key]).forEach((item) => { + processWidgetItemForReplacement(widgetModel[key], item); + }); + + return widgetModel[key]; + } + + // If we have something else like a number, boolean, etc. just return it + return widgetModel[key]; + } + + // 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 regexMatches = [...str.matchAll(regexExp)]; + const globalStateMatches = regexMatches.filter(match => { + return match[1] === dynamicStrings.GLOBAL_STATE; + }) + + // 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 = useMainStore().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 stringWithReplacement = str.replace(match[0], storeState); + + // If we still have values we need to substitute, call this function again. + if (stringWithReplacement.includes(dynamicStrings.GLOBAL_STATE)) { + return mapStringToState(stringWithReplacement); + } + + // Concatenate the string. + stringBuilder = `${stringBuilder} ${stringWithReplacement}`; + } + + return stringBuilder.trimStart(); + } \ No newline at end of file diff --git a/src/helpers/layout-helper.js b/src/helpers/layout-helper.js new file mode 100644 index 00000000..a7a31119 --- /dev/null +++ b/src/helpers/layout-helper.js @@ -0,0 +1,28 @@ +export function settleAllPromises(promiseResultMap) { + // Pull our keys out of the promise 'table' + const promiseNames = Object.entries(promiseResultMap); + + return Promise.allSettled( + promiseNames.map((e) => e[1]).map((n) => n.promise) + ).then((results) => { + const resultMap = {}; + + // Build a map of the results + for (let i = 0; i < results.length; ++i) { + const promiseName = promiseNames[i][1].resultKey; + + // Some Promises like the cms content call don't have a 'data' field + // when returned, so other promises do. Map the results to the object + // so that the object is the return data. + + if (results[i]?.value?.data === undefined) { + resultMap[promiseName] = results[i]?.value; + } else { + resultMap[promiseName] = results[i]?.value?.data; + } + } + + return resultMap; + }); + } + \ No newline at end of file diff --git a/src/helpers/layout-helper.spec.js b/src/helpers/layout-helper.spec.js new file mode 100644 index 00000000..d7181075 --- /dev/null +++ b/src/helpers/layout-helper.spec.js @@ -0,0 +1,25 @@ +import { settleAllPromises } from "@/helpers/layout-helper"; + +it("layout-helper: Should settle all promises and return mapped promise results", () => { + // Arrange + const mockPromiseOne = Promise.resolve({ data: "test-data" }); + const mockPromiseTwo = Promise.resolve({ data: "test-data-two" }); + + const promiseResultMap = [ + { + resultKey: "MockResultOne", + promise: mockPromiseOne, + }, + { + resultKey: "MockResultTwo", + promise: mockPromiseTwo, + }, + ]; + + // Act + settleAllPromises(promiseResultMap).then((results) => { + // Assert + expect(results.MockResultOne).toEqual("test-data"); + expect(results.MockResultTwo).toEqual("test-data-two"); + }); +}); diff --git a/src/layouts/vehicle-year/vehicle-year.vue b/src/layouts/vehicle-year/vehicle-year.vue index 29d5a40b..630d7ddd 100644 --- a/src/layouts/vehicle-year/vehicle-year.vue +++ b/src/layouts/vehicle-year/vehicle-year.vue @@ -3,15 +3,54 @@

Vehicle Year Placeholder

+ + {{returnVehicleYear}} +
\ No newline at end of file diff --git a/src/main.js b/src/main.js index 2039c59a..be212100 100644 --- a/src/main.js +++ b/src/main.js @@ -1,18 +1,18 @@ import { createApp } from 'vue'; -import { createPinia } from "pinia"; import App from './App.vue'; import router from './router'; import "../node_modules/bootstrap/dist/js/bootstrap.js"; import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; -import { useMainStore } from './store'; +import { useMainStore } from '@/store'; import baseMixin from "@/mixins/base-mixin.js"; +import { createPinia } from 'pinia'; const vueApp = createApp(App); // Pinia const pinia = createPinia(); -pinia.use(piniaPluginPersistedstate); vueApp.use(pinia); +pinia.use(piniaPluginPersistedstate); useMainStore().populateInitialState(); // Additional Vue items to setup diff --git a/src/mixins/base-mixin.js b/src/mixins/base-mixin.js index b8f6115f..83fd9097 100644 --- a/src/mixins/base-mixin.js +++ b/src/mixins/base-mixin.js @@ -3,10 +3,9 @@ import { navigationScenarios } from "@/router/router-constants/navigation-scenar import { vehicleCategories } from "@/constants/vehicle-categories.js"; import { queryStrings } from "@/constants/query-strings"; import { dynamicStrings } from "@/constants/dynamic-strings"; -import { useMainStore } from "../store"; +import { useMainStore } from "@/store"; import { mapStores } from "pinia"; - export default { data() { return { diff --git a/src/router/index.js b/src/router/index.js index ab5cae25..ff0f0e2f 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -1,7 +1,5 @@ import { createWebHistory, createRouter } from "vue-router"; import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader"; -import { endpoints } from "@/constants/mock-endpoints"; -import globalMethods from "@/global-methods"; import {issPageValues} from '@/router/router-constants/issPage-values'; import { routingTable } from "@/router/router-constants/routing-table"; import { useMainStore } from '@/store'; @@ -56,15 +54,8 @@ const router = createRouter({ // Get route information by page name. // This will reach out to the Cms and there is a 1:1 relationship between page names and route names. async function GetRouteInfoFromPageName(pageName) { - - //move to store actions later? - /* - const response = await store.dispatch(storeActions.GET_ROUTE_INFO_ACTION, { - pageName: pageName, - }); - */ + const response = await useMainStore().getRouteInfo(pageName); - const response = await globalMethods.mockCallHttpClient("GET", endpoints.GetRouteInfo.url + "?route=" + pageName) const jsonFromResponse = JSON.parse(response.data.Result); let routeData = []; diff --git a/src/store/index.js b/src/store/index.js index c78651c0..56668172 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1,4 +1,7 @@ -import { defineStore } from "pinia"; +import { defineStore, createPinia } from "pinia"; +import { endpoints } from "@/constants/endpoints.js"; +import globalMethods from "@/global-methods"; +import { applicationConfig } from "@/constants/application-config"; const storeId = 'main'; @@ -47,13 +50,39 @@ export const useMainStore = defineStore({ getters: {}, actions: { - // Vehicle + // Content API Actions + getRouteInfo(pageName) { + return globalMethods.callHttpClient({ + method: endpoints.GetRouteInfo.method, + endpoint: endpoints.GetRouteInfo.url(applicationConfig.APPLICATION_ABBREVIATION), + payload: { + pageName: pageName, + }, + }); + }, + getHomepageName() { + return globalMethods.callHttpClient({ + method: endpoints.GetHomepageInfo.method, + endpoint: endpoints.GetHomepageInfo.url(applicationConfig.APPLICATION_ABBREVIATION), + }); + }, + getPageData(pageName) { + return globalMethods.callHttpClient({ + method: endpoints.GetPageData.method, + endpoint: endpoints.GetPageData.url(applicationConfig.APPLICATION_ABBREVIATION, pageName), + payload: {}, + }); + }, + + // Vehicle API Actions updateVehicleYear(year) { - if (this.order.vehicle.year !== year) { - this.order.vehicle.year = year; - } + if (this.order.vehicle.year !== year) { + this.order.vehicle.year = year; + } }, + + // populate initial state populateInitialState() { if(!localStorage.getItem(storeId)) { @@ -63,5 +92,3 @@ export const useMainStore = defineStore({ }, persist: true }); - - \ No newline at end of file diff --git a/vue.config.js b/vue.config.js index 9734caad..ccad75f0 100644 --- a/vue.config.js +++ b/vue.config.js @@ -1,4 +1,4 @@ -process.env.VUE_APP_CONSUMER_CF_DISTRO ="https://digitalapi.dev.sagaws.net"; +process.env.VUE_APP_CONSUMER_CF_DISTRO ="https://digitalapi.dev.safelite.io"; process.env.VUE_APP_CURRENT_ENVIRONMENT = "Localhost"; module.exports = {