Merge pull request #12 from Safelite/feature/Digital/SSR-65

Feature/digital/ssr 65
This commit is contained in:
Kulbhushan Kaushik 2022-10-17 08:31:49 -04:00 committed by GitHub
commit 56e03f6802
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 351 additions and 39 deletions

View file

@ -75,4 +75,8 @@ stages:
clearFolder: true clearFolder: true
deployFolder: '' deployFolder: ''
region: us-east-2 region: us-east-2
appDeployVariables:
__VUE_APP_CONSUMER_CF_DISTRO__: $(__VUE_APP_CONSUMER_CF_DISTRO__)
__VUE_APP_CURRENT_ENVIRONMENT__: $(__VUE_APP_CURRENT_ENVIRONMENT__)
cfDistributionId: $(cfDistributionId) cfDistributionId: $(cfDistributionId)

View file

@ -2,7 +2,8 @@ const applicationConfig = {
CURRENT_ENVIRONMENT: process.env.VUE_APP_CURRENT_ENVIRONMENT, // "Localhost", "Dev", "QA", and "Prod" CURRENT_ENVIRONMENT: process.env.VUE_APP_CURRENT_ENVIRONMENT, // "Localhost", "Dev", "QA", and "Prod"
CONSUMER_CF_DISTRO: process.env.VUE_APP_CONSUMER_CF_DISTRO, CONSUMER_CF_DISTRO: process.env.VUE_APP_CONSUMER_CF_DISTRO,
APPLICATION_NAME: "SelfService", APPLICATION_NAME: "SelfService",
SITE_ENTRY_TRIGGER_VALUE: "SelfService" SITE_ENTRY_TRIGGER_VALUE: "SelfService",
APPLICATION_ABBREVIATION: "iss",
}; };
export { applicationConfig }; export { applicationConfig };

View file

@ -1,17 +1,17 @@
const endpoints = { const endpoints = {
GetRouteInfo: { GetRouteInfo: {
url: "/content/api/v1/content/RouteInfo", url: (applicationAbbreviation) => `/content/api/v1/content/${applicationAbbreviation}/RouteInfo`,
method: "POST", method: "POST",
}, },
GetHomepageInfo: { GetHomepageInfo: {
url: "/content/api/v1/content/HomepageInfo", url: (applicationAbbreviation) => `/content/api/v1/content/${applicationAbbreviation}/HomepageInfo`,
method: "GET", method: "GET",
}, },
GetPageData: { GetPageData: {
url: "/content/api/v1/content", url: (applicationAbbreviation, pageName) => `/content/api/v1/content/${applicationAbbreviation}/${pageName}`,
method: "GET", method: "GET",
}, },
}; };
export { endpoints }; export { endpoints };

View file

@ -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
};
}

View file

@ -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();
}

View file

@ -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;
});
}

View file

@ -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");
});
});

View file

@ -3,15 +3,54 @@
<div class="fade-on-route-transition"> <div class="fade-on-route-transition">
<navButton type="button" text="back" :scenario="this.navigationScenarios.CLICKED_BACK"></navButton> <navButton type="button" text="back" :scenario="this.navigationScenarios.CLICKED_BACK"></navButton>
<p>Vehicle Year Placeholder</p> <p>Vehicle Year Placeholder</p>
<input type="text" v-model="year"/>
<span>{{returnVehicleYear}}</span>
<button @click="updateVehicleYear(year)">Add</button>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import navButton from '@/common-components/nav-button/nav-button.vue'; import navButton from '@/common-components/nav-button/nav-button.vue';
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
export default { export default {
name: 'vehicle-year', name: 'vehicle-year',
components: { navButton }, components: { navButton },
async beforeRouteEnter(to, from, next)
{
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
// Settle promises and get results
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},
];
//use resultMap to populate layout content.
let resultMap = await settleAllPromises(promiseResultMap);
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
});
},
methods:
{
updateVehicleYear(item)
{
this.mainStore.updateVehicleYear(item);
}
},
computed:
{
returnVehicleYear()
{
return this.mainStore.order.vehicle.year;
}
}
} }
</script> </script>

View file

@ -1,18 +1,18 @@
import { createApp } from 'vue'; import { createApp } from 'vue';
import { createPinia } from "pinia";
import App from './App.vue'; import App from './App.vue';
import router from './router'; import router from './router';
import "../node_modules/bootstrap/dist/js/bootstrap.js"; import "../node_modules/bootstrap/dist/js/bootstrap.js";
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
import { useMainStore } from './store'; import { useMainStore } from '@/store';
import baseMixin from "@/mixins/base-mixin.js"; import baseMixin from "@/mixins/base-mixin.js";
import { createPinia } from 'pinia';
const vueApp = createApp(App); const vueApp = createApp(App);
// Pinia // Pinia
const pinia = createPinia(); const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
vueApp.use(pinia); vueApp.use(pinia);
pinia.use(piniaPluginPersistedstate);
useMainStore().populateInitialState(); useMainStore().populateInitialState();
// Additional Vue items to setup // Additional Vue items to setup

View file

@ -3,10 +3,9 @@ import { navigationScenarios } from "@/router/router-constants/navigation-scenar
import { vehicleCategories } from "@/constants/vehicle-categories.js"; import { vehicleCategories } from "@/constants/vehicle-categories.js";
import { queryStrings } from "@/constants/query-strings"; import { queryStrings } from "@/constants/query-strings";
import { dynamicStrings } from "@/constants/dynamic-strings"; import { dynamicStrings } from "@/constants/dynamic-strings";
import { useMainStore } from "../store"; import { useMainStore } from "@/store";
import { mapStores } from "pinia"; import { mapStores } from "pinia";
export default { export default {
data() { data() {
return { return {

View file

@ -1,7 +1,5 @@
import { createWebHistory, createRouter } from "vue-router"; import { createWebHistory, createRouter } from "vue-router";
import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader"; 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 {issPageValues} from '@/router/router-constants/issPage-values';
import { routingTable } from "@/router/router-constants/routing-table"; import { routingTable } from "@/router/router-constants/routing-table";
import { useMainStore } from '@/store'; import { useMainStore } from '@/store';
@ -56,15 +54,8 @@ const router = createRouter({
// Get route information by page name. // 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. // This will reach out to the Cms and there is a 1:1 relationship between page names and route names.
async function GetRouteInfoFromPageName(pageName) { async function GetRouteInfoFromPageName(pageName) {
const response = await useMainStore().getRouteInfo(pageName);
//move to store actions later?
/*
const response = await store.dispatch(storeActions.GET_ROUTE_INFO_ACTION, {
pageName: pageName,
});
*/
const response = await globalMethods.mockCallHttpClient("GET", endpoints.GetRouteInfo.url + "?route=" + pageName)
const jsonFromResponse = JSON.parse(response.data.Result); const jsonFromResponse = JSON.parse(response.data.Result);
let routeData = []; let routeData = [];

View file

@ -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'; const storeId = 'main';
@ -47,13 +50,39 @@ export const useMainStore = defineStore({
getters: {}, getters: {},
actions: 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) updateVehicleYear(year)
{ {
if (this.order.vehicle.year !== year) { if (this.order.vehicle.year !== year) {
this.order.vehicle.year = year; this.order.vehicle.year = year;
} }
}, },
// populate initial state
populateInitialState() populateInitialState()
{ {
if(!localStorage.getItem(storeId)) { if(!localStorage.getItem(storeId)) {
@ -63,5 +92,3 @@ export const useMainStore = defineStore({
}, },
persist: true persist: true
}); });

View file

@ -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"; process.env.VUE_APP_CURRENT_ENVIRONMENT = "Localhost";
module.exports = { module.exports = {