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
deployFolder: ''
region: us-east-2
cfDistributionId: $(cfDistributionId)
appDeployVariables:
__VUE_APP_CONSUMER_CF_DISTRO__: $(__VUE_APP_CONSUMER_CF_DISTRO__)
__VUE_APP_CURRENT_ENVIRONMENT__: $(__VUE_APP_CURRENT_ENVIRONMENT__)
cfDistributionId: $(cfDistributionId)

View file

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

View file

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

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">
<navButton type="button" text="back" :scenario="this.navigationScenarios.CLICKED_BACK"></navButton>
<p>Vehicle Year Placeholder</p>
<input type="text" v-model="year"/>
<span>{{returnVehicleYear}}</span>
<button @click="updateVehicleYear(year)">Add</button>
</div>
</div>
</template>
<script>
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 {
name: 'vehicle-year',
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>

View file

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

View file

@ -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 {

View file

@ -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 = [];

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

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";
module.exports = {