Merge pull request #28 from Safelite/feature/dynamic-routing

Routing changes
This commit is contained in:
bmauger 2021-11-02 09:09:56 -04:00 committed by GitHub
commit 7faa638b51
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1200 additions and 243 deletions

971
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -14,6 +14,7 @@
"bootstrap": "^5.1.1", "bootstrap": "^5.1.1",
"canvas-confetti": "^1.4.0", "canvas-confetti": "^1.4.0",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"http-status-codes": "^2.1.4",
"vue": "^3.0.0", "vue": "^3.0.0",
"vue-router": "^4.0.11", "vue-router": "^4.0.11",
"vuex": "^4.0.2", "vuex": "^4.0.2",

View file

@ -1,69 +1,4 @@
<template> <template>
<router-view></router-view> <router-view></router-view>
<div class="container-fluid">
<div class="row">
<div class="col">
<p class="text-center text-secondary fs-5 mb-4">
Select a year to get started
</p>
<p class="text-center fs-6 fw-bold">What year is your vehichle?</p>
</div>
</div>
<div class="row mt-5">
<radioCard
radioLabel="Windshield"
radioImage="windshield-damage.svg"
altText="Windshield"
groupName="damageKey"
radioID="windshield"
/>
<radioCard
radioLabel="Side Window"
radioImage="side-window-damage.svg"
altText="Side Window"
groupName="damageKey"
radioID="sidewindow"
/>
<radioCard
radioLabel="Back Glass"
radioImage="back-glass-damage.svg"
altText="Back Glass"
groupName="damageKey"
radioID="backglass"
/>
</div>
<div class="row">
<div class="col my-3 d-flex align-items-center">
<buttonPrimary
buttonText="Primary"
/>
</div>
</div>
<div class="row">
<div class="col my-3 d-flex align-items-center">
<listButton
buttonText="List Button"
/>
</div>
</div>
<div class="row">
<div class="col my-3 d-flex align-items-center">
<a href="#">Text Link</a>
</div>
</div>
</div>
</template> </template>
<script>
import buttonPrimary from "@/uxComponents/buttonPrimary/buttonPrimary";
import radioCard from "@/uxComponents/radioCard/radioCard";
import listButton from "@/uxComponents/listButton/listButton";
export default {
name: "App",
components: {
buttonPrimary,
radioCard,
listButton
},
};
</script>

View file

@ -1,28 +1,24 @@
<template> <template>
<form class="list-view d-flex"> <form class="list-view d-flex">
<div class="car_list"> <div class="car_list">
<div v-for="attritbute in attritbutes" :key="attritbute" class="mb-2"> <div v-for="attritbute in attritbutes" :key="attritbute" class="mb-2">
<buttonPrimary <buttonPrimary :buttonText="attritbute" v-on:click="selectAttribute(attritbute)" buttonType="btn-funnel" />
:buttonText="attritbute" </div>
v-on:click="selectAttribute(attritbute)"
buttonType="btn-funnel"
/>
</div>
</div> </div>
</form> </form>
</template> </template>
<script> <script>
import buttonPrimary from "@/uxComponents/buttonPrimary/buttonPrimary"; import buttonPrimary from "@/uxComponents/buttonPrimary/buttonPrimary";
export default { export default {
name: "CarAttributes", name: "CarAttributes",
props: { props: {
attritbutes: Array, attritbutes: Array,
selectAttribute: Function, selectAttribute: Function,
}, },
components: { components: {
buttonPrimary, buttonPrimary,
}, },
}; };
</script> </script>

View file

@ -0,0 +1,5 @@
const applicationConfig = {
}
export { applicationConfig }

View file

@ -0,0 +1,8 @@
const endpoints = {
GetRouteInfoEndpoint: {
url: '/content/api/v1/content/RouteInfo',
method: 'POST'
}
}
export { endpoints }

View file

@ -0,0 +1,5 @@
const storeActions = {
GET_ROUTE_INFO_ACTION: "getRouteInfo",
}
export { storeActions }

31
src/global-methods.js Normal file
View file

@ -0,0 +1,31 @@
import axios from "axios";
export default {
callHttpClient({ method, endpoint, payload, responseType = {} }) {
return new Promise((resolve, reject) => {
const apiGatewayUrl = "https://consumerapidev.safelite.com"; // TODO: Some environment variable
const payloadAndAnalyticsData = Object.assign({}, payload, {
AppName: "FixMyGlass",
});
axios({
method: method,
url: `${apiGatewayUrl}${endpoint}`,
data: payloadAndAnalyticsData,
crossDomain: true,
responseType: responseType,
}).then(
(response) => {
if (response.data.isSuccess || response.status == 200) {
resolve(response);
} else {
reject(response);
}
},
(error) => {
return reject(error.response);
}
);
});
},
};

View file

@ -0,0 +1,60 @@
<template>
<div class="container-fluid">
<div class="row mt-5">
<radioCard
radioLabel="Windshield"
radioImage="windshield-damage.svg"
altText="Windshield"
groupName="damageKey"
radioID="windshield"
/>
<radioCard
radioLabel="Side Window"
radioImage="side-window-damage.svg"
altText="Side Window"
groupName="damageKey"
radioID="sidewindow"
/>
<radioCard
radioLabel="Back Glass"
radioImage="back-glass-damage.svg"
altText="Back Glass"
groupName="damageKey"
radioID="backglass"
/>
</div>
<div class="row">
<div class="col my-3 d-flex align-items-center">
<buttonPrimary
buttonText="Primary"
/>
</div>
</div>
<div class="row">
<div class="col my-3 d-flex align-items-center">
<listButton
buttonText="List Button"
/>
</div>
</div>
<div class="row">
<div class="col my-3 d-flex align-items-center">
<a href="#">Text Link</a>
</div>
</div>
</div>
</template>
<script>
import buttonPrimary from "@/uxComponents/buttonPrimary/buttonPrimary";
import radioCard from "@/uxComponents/radioCard/radioCard";
import listButton from "@/uxComponents/listButton/listButton";
export default {
name: "App",
components: {
buttonPrimary,
radioCard,
listButton
},
};
</script>

View file

@ -1 +0,0 @@
test.todo('some test to be written in the future');

View file

@ -1,6 +0,0 @@
<template>
<div class="home">
<h1>Home</h1>
<router-link to="/select-car">SelectCar</router-link>
</div>
</template>

View file

@ -1 +0,0 @@
test.todo('some test to be written in the future');

View file

@ -1,5 +1,3 @@
<template> <template>
<div> <p> Not Found .... :( </p>
<p>Not found</p> </template>
</div>
</template>

View file

@ -1,37 +1,35 @@
<template> <template>
<div> <div>
<baseTemplate <baseTemplate :currentCarInfo="year" neededCarInfo="What make is your vehicle?" :backFunction="goBack">
:currentCarInfo="year" <carAttributes :attritbutes="makes" :selectAttribute="selectMake" />
neededCarInfo="What make is your vehicle?"
:backFunction="goBack"
>
<carAttributes :attritbutes="makes" :selectAttribute="selectMake" />
</baseTemplate> </baseTemplate>
</div> </div>
</template> </template>
<script> <script>
import baseTemplate from "@/commonComponents/baseTemplate/baseTemplate"; import baseTemplate from "@/commonComponents/baseTemplate/baseTemplate";
import carAttributes from "@/commonComponents/carAttributes/carAttributes"; import carAttributes from "@/commonComponents/carAttributes/carAttributes";
import { mapState } from "vuex"; import {
mapState
} from "vuex";
export default { export default {
name: "SelectCar", name: "SelectCar",
computed: { computed: {
...mapState(["year", "makes"]), ...mapState(["year", "makes"]),
},
methods: {
selectMake(make) {
this.$store.dispatch("selectMake", make);
}, },
goBack() { methods: {
this.$store.state.slideTransition = "slide_reverse"; selectMake(make) {
this.$router.push("/select-year"); this.$store.dispatch("selectMake", make);
},
goBack() {
this.$store.state.slideTransition = "slide_reverse";
this.$router.push("/select-year");
},
},
components: {
baseTemplate,
carAttributes,
}, },
},
components: {
baseTemplate,
carAttributes,
},
}; };
</script> </script>

View file

@ -1,12 +1,20 @@
import { createApp } from "vue"; import { createApp } from "vue";
import App from "./App.vue"; import App from "./App.vue";
import router from "./router"; import router from './router';
import store from "@/store"; import store from '@/store';
import baseMixin from "@/mixins/baseMixin.js";
//Bootstrap JavaScript //Bootstrap JavaScript
import "../node_modules/bootstrap/dist/js/bootstrap.js"; import "../node_modules/bootstrap/dist/js/bootstrap.js";
//Get Bootstrap and Custom Styles //Get Bootstrap and Custom Styles
import "./assets/scss/custom.scss"; import "./assets/scss/custom.scss";
createApp(App).use(router).use(store).mount("#app"); // Vue App Setup
const vueApp = createApp(App);
vueApp.use(router);
vueApp.use(store);
vueApp.mixin(baseMixin);
vueApp.mount("#app");

22
src/mixins/baseMixin.js Normal file
View file

@ -0,0 +1,22 @@
import { storeActions } from "@/constants/storeActions.js";
export default {
methods: {
dispatchBlockingStoreAction(type, payload) {
// globalMethods.showWaitingModal(true);
return this.dispatchNonBlockingStoreAction(type, payload)
.finally(() => {
// globalMethods.showWaitingModal(false);
});;
},
dispatchNonBlockingStoreAction(type, payload) {
return this.$store.dispatch(type, payload);
},
},
computed: {
storeActions() {
return storeActions;
},
},
}

View file

@ -1,3 +1,3 @@
export function lazyLoadComponent(componentName) { export function lazyLoadComponent(componentName) {
return () => import(`@/layouts/${componentName}.vue`); return () => import(`@/layouts/${componentName}/${componentName}.vue`); // /src/layouts/folder/component.vue
} }

View file

@ -1,30 +0,0 @@
import axios from "axios";
import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js";
export function getRouteData(relativeUrl) {
return axios
.post("/api/v1/content/RouteInfo", {
relativeUrl: relativeUrl,
})
.then((response) => {
// This is really only for Mockey. In a real case, this would just be a 404 and we would check status code.
if (response.data.Result === undefined) {
return Promise.reject("Could not find any details about given route.");
}
// Get our route data
let json = JSON.parse(response.data.Result);
let routeData = [];
// Push the item onto the array
Object.keys(json).forEach((key) => {
routeData.push({
path: `${json[key].RelativeUrl}`,
name: `${key}`,
component: lazyLoadComponent(json[key].LayoutName),
});
});
return Promise.resolve(routeData);
});
}

View file

@ -1,18 +1,63 @@
import { createWebHistory, createRouter } from "vue-router"; import { createWebHistory, createRouter } from "vue-router";
import { getRouteData } from "@/router/dynamic-routing/route-compile.js"; import { storeActions } from "@/constants/storeActions.js";
import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js";
import ComponentTest from "@/layouts/componentTest/componentTest.vue";
import NotFound from "@/layouts/notFound/notFound.vue"; import NotFound from "@/layouts/notFound/notFound.vue";
import Home from "@/layouts/home/home.vue"; import store from "@/store";
const routes = [ const routes = [
{ {
path: "/not-found", path: '/:pathMatch(.*)*',
name: "NotFound",
component: NotFound, component: NotFound,
name: "NotFound"
},
{
path: "/component-test", // This is a temporary route for testing.
name: "ComponentTest",
component: ComponentTest,
}, },
{ {
path: "/", path: "/",
Name: "Home", beforeEnter(to, from, next) {
component: Home,
// If we have no query string, or we don't have the FmgPage query string.
if (Object.keys(to.query).length === 0 ||to.query.fmgPage === undefined) {
RetainStructureAndGoTo404(to, next);
} else {
// If we already have our route, go to it.
if(router.hasRoute(to.query.fmgPage))
{
return next({ name: to.query.fmgPage });
}
// Get route info for the given url. Names will have a 1:1 relationship with names in the Cms.
GetRouteInfoFromPageName(to.query.fmgPage).then((routeData) => {
// Add our dynamic route.
router.addRoute({
path: "/", // Always the same path, because we control it with query strings.
name: routeData[0].name,
component: routeData[0].component,
});
// Assign current query string parameters, as well as our fmgPage one.
next({
name: routeData[0].name,
query: Object.assign(to.query, { fmgPage: routeData[0].name }),
});
})
.catch((error) => {
// If we can't find the route, go to the 404 page.
RetainStructureAndGoTo404(to, next);
console.log("error:");
console.log(error);
});
}
},
}, },
]; ];
@ -21,32 +66,42 @@ const router = createRouter({
routes, routes,
}); });
router.beforeEach(async (to, from, next) => { // Get route information by page name.
await GoToDynamicRoute(to, next); // This will reach out to the Cms and there is a 1:1 relationship between page names and route names.
}); function GetRouteInfoFromPageName(pageName) {
return new Promise((resolve, reject) => {
store
.dispatch(storeActions.GET_ROUTE_INFO_ACTION, { pageName: pageName })
.then((response) => {
// Add our route data and return our array.
let jsonFromResponse = JSON.parse(response.data.Result);
let routeData = [];
async function GoToDynamicRoute(to, next) { Object.keys(jsonFromResponse).forEach((key) => {
let doesRouteResolve = router.resolve(to.path).matched.length > 0; routeData.push({
path: `${jsonFromResponse[key].RelativeUrl}`,
name: `${key}`,
component: lazyLoadComponent(jsonFromResponse[key].LayoutName),
});
});
console.log("Does route resolve?" + " " + (doesRouteResolve ? "Yes" : "No")); resolve(routeData);
if (!doesRouteResolve) { })
let routeData = await getRouteData(to.path).catch((exp) => { .catch((error) => {
console.log(exp); reject(error);
next({ name: "NotFound" }); // Go to 404. });
}); });
}
// Add our dynamic route. // Go to our 404 page but retain our structure when we go there (path, queryString, hash).
router.addRoute({ function RetainStructureAndGoTo404(to, next){
path: routeData[0].path, next({
name: routeData[0].name, name: 'NotFound',
component: routeData[0].component, params: { pathMatch: to.path.split('/').slice(1) },
}); query: to.query,
hash: to.hash
next(to.fullPath); });
} else {
next(); // Home (/) and NotFound (/not-found) will always resolve.
}
} }
export default router; export default router;

View file

@ -1,6 +1,8 @@
import { createStore } from "vuex"; import { createStore } from "vuex";
import { endpoints } from "@/constants/endpoints.js";
import createPersistedState from "vuex-persistedstate"; import createPersistedState from "vuex-persistedstate";
import axios from "axios"; import axios from "axios";
import globalMethods from "@/global-methods";
import router from "@/router"; import router from "@/router";
export default createStore({ export default createStore({
@ -25,7 +27,7 @@ export default createStore({
mutations: { mutations: {
updateYear(state, data) { updateYear(state, data) {
state.year = data.year; state.year = data.year;
state.makes = data.makes.data.Result; state.makes = data.makes;
state.slideTransition = "slide"; state.slideTransition = "slide";
}, },
updateMake(state, data) { updateMake(state, data) {
@ -54,9 +56,16 @@ export default createStore({
}, },
actions: { actions: {
async selectYear(context, year) { async selectYear(context, year) {
const makes = await axios.get( let makes = [];
`https://mockey.qa.sagaws.net/service/makes/year=${year}` await globalMethods
); .callHttpClient({
method: "GET",
endpoint: `https://mockey.qa.sagaws.net/service/makes/year=${year}`,
payload: {},
})
.then(function (response) {
makes = response.data.Result;
});
context.commit("updateYear", { year, makes }); context.commit("updateYear", { year, makes });
router.push(`/select-make?year=${year}`); router.push(`/select-make?year=${year}`);
}, },
@ -82,5 +91,14 @@ export default createStore({
`/damage?year=${state.year}&make=${state.make}&model=${state.model}&style=${style}` `/damage?year=${state.year}&make=${state.make}&model=${state.model}&style=${style}`
); );
}, },
getRouteInfo(context, { pageName }) {
return globalMethods.callHttpClient({
method: endpoints.GetRouteInfoEndpoint.method,
endpoint: endpoints.GetRouteInfoEndpoint.url,
payload: {
pageName: pageName,
},
});
},
}, },
}); });