Merge pull request #12 from Safelite/feature/Digital/SSR-65
Feature/digital/ssr 65
This commit is contained in:
commit
56e03f6802
13 changed files with 351 additions and 39 deletions
|
|
@ -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)
|
||||
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
||||
77
src/global-methods.spec.js
Normal file
77
src/global-methods.spec.js
Normal 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
|
||||
};
|
||||
}
|
||||
121
src/helpers/cms-content-helper.js
Normal file
121
src/helpers/cms-content-helper.js
Normal 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();
|
||||
}
|
||||
28
src/helpers/layout-helper.js
Normal file
28
src/helpers/layout-helper.js
Normal 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;
|
||||
});
|
||||
}
|
||||
|
||||
25
src/helpers/layout-helper.spec.js
Normal file
25
src/helpers/layout-helper.spec.js
Normal 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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue