Merge remote-tracking branch 'origin/develop' into archived/apiRefactoring-encapsulated
This commit is contained in:
commit
904df04ac7
41 changed files with 860 additions and 616 deletions
|
|
@ -11,7 +11,7 @@ module.exports = {
|
||||||
"!src/constants/*.js",
|
"!src/constants/*.js",
|
||||||
"!src/router/**/*.js",
|
"!src/router/**/*.js",
|
||||||
"!src/helpers/unit-test-helper.js",
|
"!src/helpers/unit-test-helper.js",
|
||||||
"!src/layouts/component-test/component-test.vue"
|
"!src/layouts/component-test/component-test.vue",
|
||||||
], //! means exclude from coverage.
|
], //! means exclude from coverage.
|
||||||
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
|
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
|
||||||
coverageThreshold: {
|
coverageThreshold: {
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ import radioQuestion from "@/common-components/radio-question/radio-question";
|
||||||
describe("radioQuestion.vue", () => {
|
describe("radioQuestion.vue", () => {
|
||||||
it("Should render the 'questionText' prop value as a span value for the radio question and the 'answer' values should render as text values for radio components.", async () => {
|
it("Should render the 'questionText' prop value as a span value for the radio question and the 'answer' values should render as text values for radio components.", async () => {
|
||||||
// Act
|
// Act
|
||||||
const wrapper = shallowMount(radioQuestion)
|
const wrapper = shallowMount(radioQuestion);
|
||||||
await wrapper.setProps({
|
await wrapper.setProps({
|
||||||
questionText: "Question Text",
|
questionText: "Question Text",
|
||||||
answers: ["2023", "2022", "2021"],
|
answers: ["2023", "2022", "2021"],
|
||||||
modelValue: "2020"
|
modelValue: "2020",
|
||||||
});
|
});
|
||||||
wrapper.vm.chooseAnswer("2021");
|
wrapper.vm.chooseAnswer("2021");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="radio_question">
|
<div class="radio_question">
|
||||||
<div class="needed_car_info mt-5 mb-2 d-flex">
|
<div class="needed_car_info mt-6 mb-4 d-flex">
|
||||||
<span class="text-center fs-6 fw-bold w-100 needed_car_info-text">{{
|
<span class="text-center fs-6 fw-bold w-100 needed_car_info-text">{{
|
||||||
questionText
|
questionText
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-100 d-flex justify-content-center">
|
<div class="w-100 d-flex justify-content-center">
|
||||||
<div class="car_list overflow-scroll position-absolute container-fluid w-100 pt-1" role="radiogroup" aria-labelledby="select-year-radio-group">
|
<fieldset class="car_list overflow-scroll position-absolute container-fluid w-100 pt-1 px-5 py-0" role="radiogroup">
|
||||||
<h3 class="visually-hidden" id="select-year-radio-group">*</h3>
|
<legend class="sr-only">{{groupName}}</legend>
|
||||||
<radio v-for="answer in answers" :key="answer"
|
<radio v-for="answer in answers" :key="answer"
|
||||||
:radioID="answer"
|
:radioID="answer"
|
||||||
@click="chooseAnswer(answer)"
|
@click="chooseAnswer(answer)"
|
||||||
|
|
@ -15,11 +15,13 @@
|
||||||
loaderPosition="right"
|
loaderPosition="right"
|
||||||
sizeInRem="1.5"
|
sizeInRem="1.5"
|
||||||
data-test="radio"
|
data-test="radio"
|
||||||
groupName="radio-list"
|
:groupName="groupName"
|
||||||
textPosition="text-start"
|
textPosition="text-start"
|
||||||
|
isRequired=true
|
||||||
:value="modelValue"
|
:value="modelValue"
|
||||||
|
screenReaderOnlyText="(opens new window)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -31,12 +33,13 @@ export default {
|
||||||
props: {
|
props: {
|
||||||
questionText: String,
|
questionText: String,
|
||||||
answers: Array,
|
answers: Array,
|
||||||
modelValue: String
|
modelValue: String,
|
||||||
|
groupName: String
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
chooseAnswer(answer) {
|
chooseAnswer(answer) {
|
||||||
this.$emit('update:modelValue', answer);
|
this.$emit("update:modelValue", answer);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
radio,
|
radio,
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,19 @@
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from "@vue/test-utils";
|
||||||
import siteHeader from './site-header';
|
import siteHeader from "./site-header";
|
||||||
|
|
||||||
describe('siteHeader', () => {
|
describe("siteHeader", () => {
|
||||||
|
test("renders the logo image", () => {
|
||||||
|
// Arrange
|
||||||
|
|
||||||
test('renders the logo image', () => {
|
// Act
|
||||||
// Arrange
|
const wrapper = shallowMount(siteHeader, {
|
||||||
|
propsData: {
|
||||||
// Act
|
imageSrc: "image_url",
|
||||||
const wrapper = shallowMount(siteHeader, {
|
},
|
||||||
propsData: {
|
|
||||||
imageSrc: "image_url",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(wrapper.find('img')).toBeTruthy();
|
|
||||||
wrapper.unmount();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.find("img")).toBeTruthy();
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="funnel-header d-flex justify-content-center align-items-center" v-if="imageSrc">
|
<div
|
||||||
<img
|
class="funnel-header d-flex justify-content-center align-items-center"
|
||||||
class="logo-image img-fluid"
|
v-if="imageSrc"
|
||||||
:src="imageSrc"
|
>
|
||||||
alt="Safelite logo"
|
<img class="logo-image img-fluid" :src="imageSrc" alt="Safelite logo" />
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -14,17 +13,17 @@ export default {
|
||||||
props: {
|
props: {
|
||||||
imageSrc: {
|
imageSrc: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.funnel-header {
|
.funnel-header {
|
||||||
height: 56px;
|
height: 56px;
|
||||||
}
|
}
|
||||||
.logo-image {
|
.logo-image {
|
||||||
max-width: 78px;
|
max-width: 78px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,19 @@
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from "@vue/test-utils";
|
||||||
import vehicleBanner from './vehicle-banner';
|
import vehicleBanner from "./vehicle-banner";
|
||||||
|
|
||||||
describe('vehicleBanner', () => {
|
describe("vehicleBanner", () => {
|
||||||
|
test("renders the blurrycar image", () => {
|
||||||
|
// Arrange
|
||||||
|
|
||||||
test('renders the blurrycar image', () => {
|
// Act
|
||||||
// Arrange
|
const wrapper = shallowMount(vehicleBanner, {
|
||||||
|
propsData: {
|
||||||
// Act
|
vehicleImageSrc: "image_url",
|
||||||
const wrapper = shallowMount(vehicleBanner, {
|
},
|
||||||
propsData: {
|
|
||||||
vehicleImageSrc: "image_url",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(wrapper.find('img').attributes('class')).toContain('blurrycar');
|
|
||||||
wrapper.unmount();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.find("img").attributes("class")).toContain("blurrycar");
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="vehicle_banner mb-3 text-center">
|
<div class="vehicle_banner mb-4 text-center">
|
||||||
<img
|
<img
|
||||||
class="vehicle-image img-fluid blurrycar"
|
class="vehicle-image img-fluid blurrycar"
|
||||||
:src="vehicleImageSrc"
|
:src="vehicleImageSrc"
|
||||||
|
|
@ -14,14 +14,14 @@ export default {
|
||||||
props: {
|
props: {
|
||||||
vehicleImageSrc: {
|
vehicleImageSrc: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.vehicle-image {
|
.vehicle-image {
|
||||||
max-width: 290px;
|
max-width: 290px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
const storeMutations = {
|
const storeMutations = {
|
||||||
UPDATE_YEAR: "updateYear",
|
UPDATE_YEAR: "updateYear",
|
||||||
}
|
};
|
||||||
|
|
||||||
export { storeMutations };
|
export { storeMutations };
|
||||||
|
|
|
||||||
|
|
@ -3,27 +3,23 @@ import { widgetNames } from "@/constants/widget-names.js";
|
||||||
import store from "@/store";
|
import store from "@/store";
|
||||||
|
|
||||||
export function fetchCmsContentForPage(fmgPage) {
|
export function fetchCmsContentForPage(fmgPage) {
|
||||||
return store.dispatch(storeActions.GET_PAGE_DATA, { pageName: fmgPage }).then((response) => {
|
return store
|
||||||
const pageDataFromCms = {
|
.dispatch(storeActions.GET_PAGE_DATA, { pageName: fmgPage })
|
||||||
isCmsContentReady: false
|
.then((response) => {
|
||||||
};
|
const pageDataFromCms = {};
|
||||||
|
|
||||||
response.data.Result.forEach((widget) => {
|
response.data.Result.forEach((widget) => {
|
||||||
if (Object.values(widgetNames).includes(widget.Type)) {
|
if (Object.values(widgetNames).includes(widget.Type)) {
|
||||||
// If we already have this widget, push it on the collection
|
// If we already have this widget, push it on the collection
|
||||||
if (widget.Type in pageDataFromCms) {
|
if (widget.Type in pageDataFromCms) {
|
||||||
pageDataFromCms[widget.Type].push(widget.Model);
|
pageDataFromCms[widget.Type].push(widget.Model);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pageDataFromCms[widget.Type] = [widget.Model];
|
pageDataFromCms[widget.Type] = [widget.Model];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Our 'Page' is ready because we have data now
|
return pageDataFromCms;
|
||||||
pageDataFromCms.isCmsContentReady = true;
|
|
||||||
|
|
||||||
return pageDataFromCms;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,52 +2,51 @@ import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||||
import { dispatch } from "@/store";
|
import { dispatch } from "@/store";
|
||||||
|
|
||||||
jest.mock("@/store", () => ({
|
jest.mock("@/store", () => ({
|
||||||
dispatch: jest.fn()
|
dispatch: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it("cms-content-helper: Should return data from CMS", () => {
|
it("cms-content-helper: Should return data from CMS", () => {
|
||||||
|
// Arrange
|
||||||
|
const cmsMockData = {
|
||||||
|
Result: [
|
||||||
|
{
|
||||||
|
Type: "VehicleBannerWidget",
|
||||||
|
Model: {
|
||||||
|
ImageId: "28452dcb-7762-4cc9-ab09-7643d0b89203",
|
||||||
|
GenericVehicleImage:
|
||||||
|
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
|
||||||
|
GenericVehicleImageFilePath:
|
||||||
|
"images/default-source/default-album/blurred-image.jpg",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "PageHeaderWidget",
|
||||||
|
Model: {
|
||||||
|
HeaderText: "Select a year to get started",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "PageHeaderWidget",
|
||||||
|
Model: {
|
||||||
|
HeaderText: "Select a model",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "RadioQuestionWidget",
|
||||||
|
Model: {
|
||||||
|
QuestionText: "What year is your vehicle?",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
// Arrange
|
dispatch.mockImplementation(() => Promise.resolve({ data: cmsMockData }));
|
||||||
const cmsMockData = {
|
|
||||||
Result: [
|
|
||||||
{
|
|
||||||
Type: "VehicleBannerWidget",
|
|
||||||
Model: {
|
|
||||||
ImageId: "28452dcb-7762-4cc9-ab09-7643d0b89203",
|
|
||||||
GenericVehicleImage: "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
|
|
||||||
GenericVehicleImageFilePath: "images/default-source/default-album/blurred-image.jpg"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: "PageHeaderWidget",
|
|
||||||
Model: {
|
|
||||||
"HeaderText": "Select a year to get started"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: "PageHeaderWidget",
|
|
||||||
Model: {
|
|
||||||
"HeaderText": "Select a model"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: "RadioQuestionWidget",
|
|
||||||
Model: {
|
|
||||||
"QuestionText": "What year is your vehicle?"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch.mockImplementation(() => Promise.resolve({ data: cmsMockData }));
|
|
||||||
|
|
||||||
// Act
|
|
||||||
fetchCmsContentForPage('testPage').then((response) => {
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(response.isCmsContentReady).toBe(true);
|
|
||||||
expect(response.PageHeaderWidget[0].HeaderText).toEqual('Select a year to get started');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Act
|
||||||
|
fetchCmsContentForPage("testPage").then((response) => {
|
||||||
|
// Assert
|
||||||
|
expect(response.PageHeaderWidget[0].HeaderText).toEqual(
|
||||||
|
"Select a year to get started"
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,27 @@
|
||||||
export function settleAllPromises(promiseResultMap) {
|
export function settleAllPromises(promiseResultMap) {
|
||||||
|
// Pull our keys out of the promise 'table'
|
||||||
|
const promiseNames = Object.entries(promiseResultMap);
|
||||||
|
|
||||||
// Pull our keys out of the promise 'table'
|
return Promise.allSettled(
|
||||||
const promiseNames = Object.entries(promiseResultMap);
|
promiseNames.map((e) => e[1]).map((n) => n.promise)
|
||||||
|
).then((results) => {
|
||||||
|
const resultMap = {};
|
||||||
|
|
||||||
return Promise.allSettled(promiseNames.map(e => e[1]).map(n => n.promise))
|
// Build a map of the results
|
||||||
.then(results => {
|
for (let i = 0; i < results.length; ++i) {
|
||||||
|
const promiseName = promiseNames[i][1].resultKey;
|
||||||
|
|
||||||
const resultMap = {};
|
// 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.
|
||||||
|
|
||||||
// Build a map of the results
|
if (results[i].value.data === undefined) {
|
||||||
for (let i = 0; i < results.length; ++i) {
|
resultMap[promiseName] = results[i].value;
|
||||||
|
} else {
|
||||||
const promiseName = promiseNames[i][1].resultKey;
|
resultMap[promiseName] = results[i].value.data;
|
||||||
|
}
|
||||||
// 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) {
|
return resultMap;
|
||||||
resultMap[promiseName] = results[i].value
|
});
|
||||||
} else {
|
}
|
||||||
resultMap[promiseName] = results[i].value.data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return resultMap;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,25 @@
|
||||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||||
|
|
||||||
it("layout-helper: Should settle all promises and return mapped promise results", () => {
|
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" });
|
||||||
|
|
||||||
// Arrange
|
const promiseResultMap = [
|
||||||
const mockPromiseOne = Promise.resolve({ data: "test-data" });
|
{
|
||||||
const mockPromiseTwo = Promise.resolve({ data: "test-data-two" });
|
resultKey: "MockResultOne",
|
||||||
|
promise: mockPromiseOne,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resultKey: "MockResultTwo",
|
||||||
|
promise: mockPromiseTwo,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const promiseResultMap = [
|
// Act
|
||||||
{
|
settleAllPromises(promiseResultMap).then((results) => {
|
||||||
resultKey: "MockResultOne",
|
// Assert
|
||||||
promise: mockPromiseOne,
|
expect(results.MockResultOne).toEqual("test-data");
|
||||||
},
|
expect(results.MockResultTwo).toEqual("test-data-two");
|
||||||
{
|
});
|
||||||
resultKey: "MockResultTwo",
|
});
|
||||||
promise: mockPromiseTwo,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Act
|
|
||||||
settleAllPromises(promiseResultMap).then(results => {
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(results.MockResultOne).toEqual('test-data');
|
|
||||||
expect(results.MockResultTwo).toEqual('test-data-two');
|
|
||||||
});
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
|
||||||
|
|
@ -2,35 +2,35 @@
|
||||||
<div class="container-fluid container-shadow p-2 rounded-3">
|
<div class="container-fluid container-shadow p-2 rounded-3">
|
||||||
<div class="row my-4">
|
<div class="row my-4">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h4 class="m-0 p-2 bg-light rounded">Radio Card</h4>
|
<h4 class="m-0 p-2 bg-light rounded">Radio Card</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
<radioCard
|
<radioCard
|
||||||
radioLabel="Windshield"
|
radioLabel="Windshield"
|
||||||
radioImage="windshield-damage.svg"
|
radioImage="windshield-damage.svg"
|
||||||
altText="Windshield"
|
altText="Windshield"
|
||||||
groupName="damageKey"
|
groupName="damageKey"
|
||||||
radioID="windshield"
|
radioID="windshield"
|
||||||
/>
|
/>
|
||||||
<radioCard
|
<radioCard
|
||||||
radioLabel="Side Window"
|
radioLabel="Side Window"
|
||||||
radioImage="side-window-damage.svg"
|
radioImage="side-window-damage.svg"
|
||||||
altText="Side Window"
|
altText="Side Window"
|
||||||
groupName="damageKey"
|
groupName="damageKey"
|
||||||
radioID="sidewindow"
|
radioID="sidewindow"
|
||||||
/>
|
/>
|
||||||
<radioCard
|
<radioCard
|
||||||
radioLabel="Back Glass"
|
radioLabel="Back Glass"
|
||||||
radioImage="back-glass-damage.svg"
|
radioImage="back-glass-damage.svg"
|
||||||
altText="Back Glass"
|
altText="Back Glass"
|
||||||
groupName="damageKey"
|
groupName="damageKey"
|
||||||
radioID="backglass"
|
radioID="backglass"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="row my-4">
|
<div class="row my-4">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h4 class="m-0 p-2 bg-light rounded">Buttons</h4>
|
<h4 class="m-0 p-2 bg-light rounded">Buttons</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
@ -38,84 +38,106 @@
|
||||||
<buttonPrimary
|
<buttonPrimary
|
||||||
buttonText="Primary"
|
buttonText="Primary"
|
||||||
loaderColor="white"
|
loaderColor="white"
|
||||||
sizeInRem="1"
|
sizeInRem="1" />
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col my-3 d-flex align-items-center">
|
<div class="col my-3 d-flex align-items-center">
|
||||||
<buttonSecondary
|
<buttonSecondary
|
||||||
buttonText="Secondary"
|
buttonText="Secondary"
|
||||||
loaderColor="white"
|
loaderColor="white"
|
||||||
sizeInRem="1"
|
sizeInRem="1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col my-3 d-flex align-items-center">
|
||||||
|
<buttonBack/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row my-4">
|
<div class="row my-4">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h4 class="m-0 p-2 bg-light rounded">List Button</h4>
|
<h4 class="m-0 p-2 bg-light rounded">List Button</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col my-3 d-flex align-items-center">
|
<div class="col my-3 d-flex align-items-center">
|
||||||
<listButton
|
<listButton
|
||||||
buttonText="List Button"
|
buttonText="List Button"
|
||||||
errorText="Test error message"
|
errorText="Test error message"
|
||||||
loaderColor="blue"
|
loaderColor="blue"
|
||||||
loaderPosition="right"
|
loaderPosition="right"
|
||||||
sizeInRem="1"
|
sizeInRem="1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row my-4">
|
<div class="row my-4">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h4 class="m-0 p-2 bg-light rounded">Radio - Single-Line</h4>
|
<h4 class="m-0 p-2 bg-light rounded">Radio - Single-Line</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- The role="radiogroup" and aria-labelledby must be included in the parent component for the radio group -->
|
<!-- The role="radiogroup" and aria-labelledby must be included in the parent component for the radio group -->
|
||||||
<div role="radiogroup" aria-labelledby="demo-1-radio-group" class="col my-3 d-flex align-items-center flex-column">
|
<div
|
||||||
|
role="radiogroup"
|
||||||
|
aria-labelledby="demo-1-radio-group"
|
||||||
|
class="col my-3 d-flex align-items-center flex-column"
|
||||||
|
>
|
||||||
<!-- The h3 and id must be included. The id must match the aria-labelledby of the parent div. -->
|
<!-- The h3 and id must be included. The id must match the aria-labelledby of the parent div. -->
|
||||||
<h3 class="visually-hidden" id="demo-1-radio-group">Select Vehicle Year</h3>
|
<h3 class="sr-only" id="demo-1-radio-group">
|
||||||
|
Select Vehicle Year
|
||||||
|
</h3>
|
||||||
<radio
|
<radio
|
||||||
groupName="demo-1"
|
groupName="demo-1"
|
||||||
ariaLabelBy="vehicle-year"
|
ariaLabelBy="vehicle-year"
|
||||||
radioID="2021"
|
radioID="2021"
|
||||||
|
isRequired=true
|
||||||
textPosition="text-start"
|
textPosition="text-start"
|
||||||
loaderColor="blue"
|
loaderColor="blue"
|
||||||
loaderPosition="right"
|
loaderPosition="right"
|
||||||
sizeInRem="1"
|
sizeInRem="1"
|
||||||
|
screenReaderOnlyText="(opens new window)"
|
||||||
/>
|
/>
|
||||||
<radio
|
<radio
|
||||||
groupName="demo-1"
|
groupName="demo-1"
|
||||||
ariaLabelBy="vehicle-year"
|
ariaLabelBy="vehicle-year"
|
||||||
radioID="2020"
|
radioID="2020"
|
||||||
|
isRequired=true
|
||||||
textPosition="text-start"
|
textPosition="text-start"
|
||||||
loaderColor="blue"
|
loaderColor="blue"
|
||||||
loaderPosition="right"
|
loaderPosition="right"
|
||||||
sizeInRem="1"
|
sizeInRem="1"
|
||||||
|
screenReaderOnlyText="(opens new window)"
|
||||||
/>
|
/>
|
||||||
<radio
|
<radio
|
||||||
groupName="demo-1"
|
groupName="demo-1"
|
||||||
ariaLabelBy="vehicle-year"
|
ariaLabelBy="vehicle-year"
|
||||||
radioID="2019"
|
radioID="2019"
|
||||||
|
isRequired=true
|
||||||
textPosition="text-start"
|
textPosition="text-start"
|
||||||
loaderColor="blue"
|
loaderColor="blue"
|
||||||
loaderPosition="right"
|
loaderPosition="right"
|
||||||
sizeInRem="1"
|
sizeInRem="1"
|
||||||
|
screenReaderOnlyText="(opens new window)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row my-4">
|
<div class="row my-4">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h4 class="m-0 p-2 bg-light rounded">Radio - Multi-Line</h4>
|
<h4 class="m-0 p-2 bg-light rounded">Radio - Multi-Line</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- The role="radiogroup" and aria-labelledby must be included in the parent component for the radio group -->
|
<!-- The role="radiogroup" and aria-labelledby must be included in the parent component for the radio group -->
|
||||||
<div role="radiogroup" aria-labelledby="demo-2-radio-group" class="col my-3 d-flex align-items-center flex-column">
|
<div
|
||||||
|
role="radiogroup"
|
||||||
|
aria-labelledby="demo-2-radio-group"
|
||||||
|
class="col my-3 d-flex align-items-center flex-column"
|
||||||
|
>
|
||||||
<!-- The h3 and id must be included. The id must match the aria-labelledby of the parent div. -->
|
<!-- The h3 and id must be included. The id must match the aria-labelledby of the parent div. -->
|
||||||
<h3 class="visually-hidden" id="demo-2-radio-group">Select Vehicle Year</h3>
|
<h3 class="sr-only" id="demo-2-radio-group">
|
||||||
|
Select Vehicle Year
|
||||||
|
</h3>
|
||||||
<radio
|
<radio
|
||||||
groupName="demo-2"
|
groupName="demo-2"
|
||||||
ariaLabelBy="vehicle-make"
|
ariaLabelBy="vehicle-make"
|
||||||
|
|
@ -125,6 +147,7 @@
|
||||||
loaderColor="blue"
|
loaderColor="blue"
|
||||||
loaderPosition="right"
|
loaderPosition="right"
|
||||||
sizeInRem="1"
|
sizeInRem="1"
|
||||||
|
screenReaderOnlyText="(opens new window)"
|
||||||
/>
|
/>
|
||||||
<radio
|
<radio
|
||||||
groupName="demo-2"
|
groupName="demo-2"
|
||||||
|
|
@ -135,6 +158,7 @@
|
||||||
loaderColor="blue"
|
loaderColor="blue"
|
||||||
loaderPosition="right"
|
loaderPosition="right"
|
||||||
sizeInRem="1"
|
sizeInRem="1"
|
||||||
|
screenReaderOnlyText="(opens new window)"
|
||||||
/>
|
/>
|
||||||
<radio
|
<radio
|
||||||
groupName="demo-2"
|
groupName="demo-2"
|
||||||
|
|
@ -145,19 +169,26 @@
|
||||||
loaderColor="blue"
|
loaderColor="blue"
|
||||||
loaderPosition="right"
|
loaderPosition="right"
|
||||||
sizeInRem="1"
|
sizeInRem="1"
|
||||||
|
screenReaderOnlyText="(opens new window)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row my-4">
|
<div class="row my-4">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h4 class="m-0 p-2 bg-light rounded">Radio - Multi-Line Centered</h4>
|
<h4 class="m-0 p-2 bg-light rounded">Radio - Multi-Line Centered</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- The role="radiogroup" and aria-labelledby must be included in the parent component for the radio group -->
|
<!-- The role="radiogroup" and aria-labelledby must be included in the parent component for the radio group -->
|
||||||
<div role="radiogroup" aria-labelledby="demo-3-radio-group" class="col my-3 d-flex align-items-center flex-column">
|
<div
|
||||||
|
role="radiogroup"
|
||||||
|
aria-labelledby="demo-3-radio-group"
|
||||||
|
class="col my-3 d-flex align-items-center flex-column"
|
||||||
|
>
|
||||||
<!-- The h3 and id must be included. The id must match the aria-labelledby of the parent div. -->
|
<!-- The h3 and id must be included. The id must match the aria-labelledby of the parent div. -->
|
||||||
<h3 class="visually-hidden" id="demo-3-radio-group">Multi-Line Centered</h3>
|
<h3 class="sr-only" id="demo-3-radio-group">
|
||||||
|
Multi-Line Centered
|
||||||
|
</h3>
|
||||||
<radio
|
<radio
|
||||||
groupName="demo-3"
|
groupName="demo-3"
|
||||||
ariaLabelBy="vehicle-model"
|
ariaLabelBy="vehicle-model"
|
||||||
|
|
@ -167,6 +198,7 @@
|
||||||
loaderColor="blue"
|
loaderColor="blue"
|
||||||
loaderPosition="right"
|
loaderPosition="right"
|
||||||
sizeInRem="1"
|
sizeInRem="1"
|
||||||
|
screenReaderOnlyText="(opens new window)"
|
||||||
/>
|
/>
|
||||||
<radio
|
<radio
|
||||||
groupName="demo-3"
|
groupName="demo-3"
|
||||||
|
|
@ -177,6 +209,7 @@
|
||||||
loaderColor="blue"
|
loaderColor="blue"
|
||||||
loaderPosition="right"
|
loaderPosition="right"
|
||||||
sizeInRem="1"
|
sizeInRem="1"
|
||||||
|
screenReaderOnlyText="(opens new window)"
|
||||||
/>
|
/>
|
||||||
<radio
|
<radio
|
||||||
groupName="demo-3"
|
groupName="demo-3"
|
||||||
|
|
@ -187,19 +220,26 @@
|
||||||
loaderColor="blue"
|
loaderColor="blue"
|
||||||
loaderPosition="right"
|
loaderPosition="right"
|
||||||
sizeInRem="1"
|
sizeInRem="1"
|
||||||
|
screenReaderOnlyText="(opens new window)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row my-4">
|
<div class="row my-4">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h4 class="m-0 p-2 bg-light rounded">Radio Horizontal</h4>
|
<h4 class="m-0 p-2 bg-light rounded">Radio Horizontal</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row px-3">
|
<div class="row px-3">
|
||||||
<!-- The role="radiogroup" and aria-labelledby must be included in the parent component for the radio group -->
|
<!-- The role="radiogroup" and aria-labelledby must be included in the parent component for the radio group -->
|
||||||
<div role="radiogroup" aria-labelledby="demo-4-radio-group" class="d-flex flex-row p-0">
|
<div
|
||||||
|
role="radiogroup"
|
||||||
|
aria-labelledby="demo-4-radio-group"
|
||||||
|
class="d-flex flex-row p-0"
|
||||||
|
>
|
||||||
<!-- The h3 and id must be included. The id must match the aria-labelledby of the parent div. -->
|
<!-- The h3 and id must be included. The id must match the aria-labelledby of the parent div. -->
|
||||||
<h3 class="visually-hidden" id="demo-4-radio-group">Select Vehicle Year</h3>
|
<h3 class="sr-only" id="demo-4-radio-group">
|
||||||
|
Select Vehicle Year
|
||||||
|
</h3>
|
||||||
<radioHorizontal
|
<radioHorizontal
|
||||||
groupName="demo-4"
|
groupName="demo-4"
|
||||||
ariaLabelBy="vehicle-model"
|
ariaLabelBy="vehicle-model"
|
||||||
|
|
@ -211,6 +251,7 @@
|
||||||
sizeInRem="1"
|
sizeInRem="1"
|
||||||
v-bind:totalInGroup="3"
|
v-bind:totalInGroup="3"
|
||||||
v-bind:positionInGroup="1"
|
v-bind:positionInGroup="1"
|
||||||
|
screenReaderOnlyText="(opens new window)"
|
||||||
/>
|
/>
|
||||||
<radioHorizontal
|
<radioHorizontal
|
||||||
groupName="demo-4"
|
groupName="demo-4"
|
||||||
|
|
@ -223,6 +264,7 @@
|
||||||
sizeInRem="1"
|
sizeInRem="1"
|
||||||
v-bind:totalInGroup="3"
|
v-bind:totalInGroup="3"
|
||||||
v-bind:positionInGroup="2"
|
v-bind:positionInGroup="2"
|
||||||
|
screenReaderOnlyText="(opens new window)"
|
||||||
/>
|
/>
|
||||||
<radioHorizontal
|
<radioHorizontal
|
||||||
groupName="demo-4"
|
groupName="demo-4"
|
||||||
|
|
@ -235,12 +277,13 @@
|
||||||
sizeInRem="1"
|
sizeInRem="1"
|
||||||
v-bind:totalInGroup="3"
|
v-bind:totalInGroup="3"
|
||||||
v-bind:positionInGroup="3"
|
v-bind:positionInGroup="3"
|
||||||
|
screenReaderOnlyText="(opens new window)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row my-4">
|
<div class="row my-4">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h4 class="m-0 p-2 bg-light rounded">Text Link</h4>
|
<h4 class="m-0 p-2 bg-light rounded">Text Link</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
@ -250,19 +293,23 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="row my-4">
|
<div class="row my-4">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h4 class="m-0 p-2 bg-light rounded">Typogrophy</h4>
|
<h4 class="m-0 p-2 bg-light rounded">Typogrophy</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row my-2">
|
<div class="row my-2">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<p>This is default body copy font size/weight</p>
|
<p>This is default body copy font size/weight</p>
|
||||||
<p class="small">This is small body copy using <code>.small</code> class</p>
|
<p class="small">
|
||||||
<p><small>This is also small using <code><small></code> tag</small></p>
|
This is small body copy using <code>.small</code> class
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<small>This is also small using <code><small></code> tag</small>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row my-4">
|
<div class="row my-4">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h4 class="m-0 p-2 bg-light rounded">Headings</h4>
|
<h4 class="m-0 p-2 bg-light rounded">Headings</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row my-2">
|
<div class="row my-2">
|
||||||
|
|
@ -297,119 +344,125 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="row my-4">
|
<div class="row my-4">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h4 class="m-0 p-2 bg-light rounded">Alerts</h4>
|
<h4 class="m-0 p-2 bg-light rounded">Alerts</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row my-2">
|
<div class="row my-2">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<alert
|
<alert
|
||||||
alertClass="alert-success"
|
alertClass="alert-success"
|
||||||
alertHeadline="Dismissible Alert"
|
alertHeadline="Dismissible Alert"
|
||||||
alertCopy="This is an example of a DISMISSIBLE alert. It will fade away and content around it will shift when dismissed."
|
alertCopy="This is an example of a DISMISSIBLE alert. It will fade away and content around it will shift when dismissed."
|
||||||
v-bind:isDismissible = "true"
|
v-bind:isDismissible="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row my-2">
|
<div class="row my-2">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<alert
|
<alert
|
||||||
alertClass="alert-danger"
|
alertClass="alert-danger"
|
||||||
alertHeadline="NON-Dismissible Alert"
|
alertHeadline="NON-Dismissible Alert"
|
||||||
alertCopy="This is an example of a NON-DISMISSIBLE alert."
|
alertCopy="This is an example of a NON-DISMISSIBLE alert."
|
||||||
v-bind:isDismissible = "false"
|
v-bind:isDismissible="false"
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row my-2">
|
|
||||||
<div class="col">
|
|
||||||
<alert
|
|
||||||
alertClass="alert-warning"
|
|
||||||
alertHeadline="Warning Alert"
|
|
||||||
alertCopy="This is an example of a WARNING alert."
|
|
||||||
v-bind:isDismissible = "false"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row my-2">
|
<div class="row my-2">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<alert
|
<alert
|
||||||
alertClass="alert-info"
|
alertClass="alert-warning"
|
||||||
alertHeadline="Info Alert"
|
alertHeadline="Warning Alert"
|
||||||
alertCopy="This is an example of a INFO alert."
|
alertCopy="This is an example of a WARNING alert."
|
||||||
v-bind:isDismissible = "false"
|
v-bind:isDismissible="false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row my-2">
|
<div class="row my-2">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<alert
|
<alert
|
||||||
alertClass="alert-danger"
|
alertClass="alert-info"
|
||||||
alertHeadline="Danger Alert"
|
alertHeadline="Info Alert"
|
||||||
alertCopy="This is an example of a DANGER alert."
|
alertCopy="This is an example of a INFO alert."
|
||||||
v-bind:isDismissible = "false"
|
v-bind:isDismissible="false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row my-2">
|
<div class="row my-2">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<alert
|
<alert
|
||||||
alertClass="alert-success"
|
alertClass="alert-danger"
|
||||||
alertHeadline="No Body Copy Alert"
|
alertHeadline="Danger Alert"
|
||||||
alertCopy=""
|
alertCopy="This is an example of a DANGER alert."
|
||||||
v-bind:isDismissible = "false"
|
v-bind:isDismissible="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row my-2">
|
||||||
|
<div class="col">
|
||||||
|
<alert
|
||||||
|
alertClass="alert-success"
|
||||||
|
alertHeadline="No Body Copy Alert"
|
||||||
|
alertCopy=""
|
||||||
|
v-bind:isDismissible="false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row my-4">
|
<div class="row my-4">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h4 class="m-0 p-2 bg-light rounded">Site Header</h4>
|
<h4 class="m-0 p-2 bg-light rounded">Site Header</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row my-3">
|
<div class="row my-3">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<siteHeader imageSrc="https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3" />
|
<siteHeader
|
||||||
|
imageSrc="https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row my-4">
|
<div class="row my-4">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h4 class="m-0 p-2 bg-light rounded">Vehicle Banner</h4>
|
<h4 class="m-0 p-2 bg-light rounded">Vehicle Banner</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row my-3">
|
<div class="row my-3">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<vehicleBanner vehicleImageSrc="https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3" />
|
<vehicleBanner
|
||||||
|
vehicleImageSrc="https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import buttonPrimary from "@/ux-components/button-primary/button-primary";
|
import buttonPrimary from "@/ux-components/button-primary/button-primary";
|
||||||
import buttonSecondary from "@/ux-components/button-secondary/button-secondary";
|
import buttonSecondary from "@/ux-components/button-secondary/button-secondary";
|
||||||
import radioCard from "@/ux-components/radio-card/radio-card";
|
import buttonBack from "@/ux-components/button-back/button-back";
|
||||||
import listButton from "@/ux-components/list-button/list-button";
|
import radioCard from "@/ux-components/radio-card/radio-card";
|
||||||
import radio from "@/ux-components/radio/radio";
|
import listButton from "@/ux-components/list-button/list-button";
|
||||||
import alert from "@/ux-components/alert/alert";
|
import radio from "@/ux-components/radio/radio";
|
||||||
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
|
import alert from "@/ux-components/alert/alert";
|
||||||
import siteHeader from "@/common-components/site-header/site-header";
|
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
|
||||||
import radioHorizontal from "@/ux-components/radio-horizontal/radio-horizontal";
|
import siteHeader from "@/common-components/site-header/site-header";
|
||||||
export default {
|
import radioHorizontal from "@/ux-components/radio-horizontal/radio-horizontal";
|
||||||
name: "App",
|
export default {
|
||||||
components: {
|
name: "App",
|
||||||
buttonPrimary,
|
components: {
|
||||||
buttonSecondary,
|
buttonPrimary,
|
||||||
radioCard,
|
buttonSecondary,
|
||||||
listButton,
|
buttonBack,
|
||||||
radio,
|
radioCard,
|
||||||
alert,
|
listButton,
|
||||||
vehicleBanner,
|
radio,
|
||||||
radioHorizontal,
|
alert,
|
||||||
siteHeader,
|
vehicleBanner,
|
||||||
},
|
radioHorizontal,
|
||||||
data() {
|
siteHeader,
|
||||||
return {
|
},
|
||||||
years: [2023, 2022, 2021, 2020],
|
data() {
|
||||||
};
|
return {
|
||||||
}
|
years: [2023, 2022, 2021, 2020],
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col my-3 d-flex align-items-center">
|
<div class="col my-3 d-flex align-items-center">
|
||||||
<loader
|
<loader sizeInRem="10" loaderColor="blue" loaderPosition="center" />
|
||||||
sizeInRem="10"
|
|
||||||
loaderColor="blue"
|
|
||||||
loaderPosition="center"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -22,7 +18,7 @@ import loader from "@/ux-components/loader/loader";
|
||||||
export default {
|
export default {
|
||||||
name: "App",
|
name: "App",
|
||||||
components: {
|
components: {
|
||||||
loader
|
loader,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,15 @@ import { shallowMount, flushPromises } from "@vue/test-utils";
|
||||||
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||||
import vehicleYear from "@/layouts/vehicle-year/vehicle-year.vue";
|
import vehicleYear from "@/layouts/vehicle-year/vehicle-year.vue";
|
||||||
import { settleAllPromises } from "@/helpers/layout-helper.js";
|
import { settleAllPromises } from "@/helpers/layout-helper.js";
|
||||||
import { nextTick } from 'vue'
|
import { nextTick } from "vue";
|
||||||
|
|
||||||
// Mock our module for promises.
|
// Mock our module for promises.
|
||||||
jest.mock("@/helpers/layout-helper.js", () => ({
|
jest.mock("@/helpers/layout-helper.js", () => ({
|
||||||
settleAllPromises: jest.fn()
|
settleAllPromises: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe("vehicle-year.vue", () => {
|
describe("vehicle-year.vue", () => {
|
||||||
test("vehicle-year.vue should render data from CMS", async () => {
|
test("vehicle-year.vue should render data from CMS", async () => {
|
||||||
|
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
||||||
// Our mock data for our call to settleAllPromises
|
// Our mock data for our call to settleAllPromises
|
||||||
|
|
@ -19,31 +18,43 @@ describe("vehicle-year.vue", () => {
|
||||||
getPageContent: {
|
getPageContent: {
|
||||||
PageHeaderWidget: [{ HeaderText: "Select a year to get started" }],
|
PageHeaderWidget: [{ HeaderText: "Select a year to get started" }],
|
||||||
RadioQuestionWidget: [{ QuestionText: "What year is your vehicle?" }],
|
RadioQuestionWidget: [{ QuestionText: "What year is your vehicle?" }],
|
||||||
VehicleBannerWidget: [{ GenericVehicleImage: "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3" }],
|
VehicleBannerWidget: [
|
||||||
SiteHeaderWidget: [{ "LogoImage": "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3" }],
|
{
|
||||||
|
GenericVehicleImage:
|
||||||
|
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
SiteHeaderWidget: [
|
||||||
|
{
|
||||||
|
LogoImage:
|
||||||
|
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
|
||||||
|
},
|
||||||
|
],
|
||||||
isCmsContentReady: true,
|
isCmsContentReady: true,
|
||||||
},
|
},
|
||||||
getVehicleYear: [2023, 2022, 2021],
|
getVehicleYear: [2023, 2022, 2021],
|
||||||
store: {
|
store: {
|
||||||
commit: jest.fn(),
|
commit: jest.fn(),
|
||||||
year: null
|
year: null,
|
||||||
},
|
},
|
||||||
router: {
|
router: {
|
||||||
push: jest.fn()
|
push: jest.fn(),
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
// our router information needed.
|
// our router information needed.
|
||||||
const to = {
|
const to = {
|
||||||
query: {
|
query: {
|
||||||
fmgPage: 'vehicle-year'
|
fmgPage: "vehicle-year",
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const mountOptions = getMountOptions(mockData);
|
const mountOptions = getMountOptions(mockData);
|
||||||
|
|
||||||
// our mock implementation of settleAllPromises
|
// our mock implementation of settleAllPromises
|
||||||
settleAllPromises.mockImplementation(() => { return Promise.resolve(mockData);});
|
settleAllPromises.mockImplementation(() => {
|
||||||
|
return Promise.resolve(mockData);
|
||||||
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const wrapper = shallowMount(vehicleYear, mountOptions);
|
const wrapper = shallowMount(vehicleYear, mountOptions);
|
||||||
|
|
@ -52,7 +63,9 @@ describe("vehicle-year.vue", () => {
|
||||||
// Call our beforeRouteEnter on the component.
|
// Call our beforeRouteEnter on the component.
|
||||||
// This passes (c) => c(wrapper.vm) so that next can be called and our
|
// This passes (c) => c(wrapper.vm) so that next can be called and our
|
||||||
// data can be set.
|
// data can be set.
|
||||||
vehicleYear.beforeRouteEnter.call(wrapper.vm, to, undefined, (c) => c(wrapper.vm));
|
vehicleYear.beforeRouteEnter.call(wrapper.vm, to, undefined, (c) =>
|
||||||
|
c(wrapper.vm)
|
||||||
|
);
|
||||||
|
|
||||||
await nextTick(); // Wait for the DOM to update.
|
await nextTick(); // Wait for the DOM to update.
|
||||||
|
|
||||||
|
|
@ -60,7 +73,9 @@ describe("vehicle-year.vue", () => {
|
||||||
const header = await wrapper.find(".Header");
|
const header = await wrapper.find(".Header");
|
||||||
expect(header.attributes("text")).toEqual("Select a year to get started");
|
expect(header.attributes("text")).toEqual("Select a year to get started");
|
||||||
|
|
||||||
const yearQuestion = wrapper.findComponent({ name: 'year-question' });
|
const yearQuestion = wrapper.findComponent({ name: "year-question" });
|
||||||
expect(yearQuestion.attributes("questiontext")).toEqual("What year is your vehicle?");
|
expect(yearQuestion.attributes("questiontext")).toEqual(
|
||||||
|
"What year is your vehicle?"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
<template v-if="isCmsContentReady">
|
<template v-if="isCmsContentReady">
|
||||||
<siteHeader :imageSrc="siteHeaderWidget.LogoImage" />
|
<div class="container-fluid shadow rounded-3 p-0">
|
||||||
<div class="select-car">
|
<siteHeader :imageSrc="siteHeaderWidget.LogoImage" />
|
||||||
<div class="select-car-form rounded text-center">
|
<div class="select-car">
|
||||||
<vehicleBanner :vehicleImageSrc="vehicleBannerWidget.GenericVehicleImage" />
|
<div class="select-car-form rounded text-center">
|
||||||
<pageHeader :text="pageHeaderWidgets.HeaderText" class="Header" />
|
<vehicleBanner :vehicleImageSrc="vehicleBannerWidget.GenericVehicleImage" />
|
||||||
<yearQuestion v-model="selectedYear" ref="yearQuestion" />
|
<pageHeader :text="pageHeaderWidgets.HeaderText" class="Header" />
|
||||||
|
<yearQuestion v-model="selectedYear" ref="yearQuestion" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -25,13 +27,13 @@ export default {
|
||||||
pageHeaderWidgets: {},
|
pageHeaderWidgets: {},
|
||||||
siteHeaderWidget: {},
|
siteHeaderWidget: {},
|
||||||
vehicleBannerWidget: {},
|
vehicleBannerWidget: {},
|
||||||
selectedYear: null
|
selectedYear: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {},
|
computed: {},
|
||||||
|
|
||||||
beforeRouteEnter(to, from, next) {
|
beforeRouteEnter(to, from, next) {
|
||||||
|
|
||||||
// Call APIs
|
// Call APIs
|
||||||
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
|
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
|
||||||
const yearQuestionInitialDataPromise = yearQuestion.methods.loadInitialData();
|
const yearQuestionInitialDataPromise = yearQuestion.methods.loadInitialData();
|
||||||
|
|
@ -61,8 +63,8 @@ export default {
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
selectedYear(year) {
|
selectedYear(year) {
|
||||||
this.$store.commit(this.storeMutations.UPDATE_YEAR, year);
|
this.$store.commit(this.storeMutations.UPDATE_YEAR, year);
|
||||||
this.$router.push('?fmgPage=vehicle-make');
|
this.$router.push('?fmgPage=vehicle-make');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -76,15 +78,13 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.select-car {
|
.select-car {
|
||||||
height: calc(100vh - 56px);
|
height: calc(100vh - 56px);
|
||||||
padding: 0 1.5rem;
|
|
||||||
|
|
||||||
.car_list {
|
.car_list {
|
||||||
// Height will be determined by overall height of content above list
|
// Height will be determined by overall height of content above list
|
||||||
height: calc(100% - 300px);
|
height: calc(100% - 300px);
|
||||||
padding: 0 1.5rem;
|
-webkit-overflow-scrolling: touch;
|
||||||
-webkit-overflow-scrolling: touch;
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,25 @@
|
||||||
import yearQuestion from "@/layouts/vehicle-year/year-question/year-question";
|
import yearQuestion from "@/layouts/vehicle-year/year-question/year-question";
|
||||||
import { shallowMount } from "@vue/test-utils";
|
import { shallowMount } from "@vue/test-utils";
|
||||||
|
|
||||||
describe('year-question.vue', () => {
|
describe("year-question.vue", () => {
|
||||||
test('year-question should take a prop for years and questionText, and trigger a selectYear function when an option is clicked.', async () => {
|
test("year-question should take a prop for years and questionText, and trigger a selectYear function when an option is clicked.", async () => {
|
||||||
// Act
|
// Act
|
||||||
const wrapper = shallowMount(yearQuestion);
|
const wrapper = shallowMount(yearQuestion);
|
||||||
await wrapper.setProps({
|
await wrapper.setProps({
|
||||||
questionText: 'What year is your vehicle?',
|
questionText: "What year is your vehicle?",
|
||||||
years: ['2023', '2022', '2021'],
|
years: ["2023", "2022", "2021"],
|
||||||
modelValue: '2020'
|
modelValue: "2020",
|
||||||
});
|
|
||||||
wrapper.vm.$options.watch.selectedYear.call(wrapper.vm);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const radioQuestion = await wrapper.findComponent({name: 'radioQuestion'});
|
|
||||||
expect(radioQuestion.attributes('questiontext')).toBe("What year is your vehicle?");
|
|
||||||
expect(radioQuestion.attributes('answers')).toBe("2023,2022,2021");
|
|
||||||
expect(wrapper.componentVM.modelValue).toBe("2020");
|
|
||||||
});
|
});
|
||||||
});
|
wrapper.vm.$options.watch.selectedYear.call(wrapper.vm);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const radioQuestion = await wrapper.findComponent({
|
||||||
|
name: "radioQuestion",
|
||||||
|
});
|
||||||
|
expect(radioQuestion.attributes("questiontext")).toBe(
|
||||||
|
"What year is your vehicle?"
|
||||||
|
);
|
||||||
|
expect(radioQuestion.attributes("answers")).toBe("2023,2022,2021");
|
||||||
|
expect(wrapper.componentVM.modelValue).toBe("2020");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,47 @@
|
||||||
<template>
|
<template>
|
||||||
<radioQuestion class="radioQuestion"
|
<radioQuestion class="radioQuestion"
|
||||||
:questionText="questionText"
|
:questionText="questionText"
|
||||||
:answers="years"
|
:answers="years"
|
||||||
|
groupName="Choose Vehicle Year"
|
||||||
v-model="selectedYear"
|
v-model="selectedYear"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Components
|
||||||
import radioQuestion from "@/common-components/radio-question/radio-question";
|
import radioQuestion from "@/common-components/radio-question/radio-question";
|
||||||
|
// Supporting files
|
||||||
import store from "@/store";
|
import store from "@/store";
|
||||||
import { storeActions } from "@/constants/store-actions.js";
|
import { storeActions } from "@/constants/store-actions.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "year-question",
|
name: "year-question",
|
||||||
data(){
|
data() {
|
||||||
return {
|
return {
|
||||||
questionText: null,
|
questionText: null,
|
||||||
selectedYear: null,
|
selectedYear: null,
|
||||||
years: Array,
|
years: Array,
|
||||||
}
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
modelValue: String
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
radioQuestion,
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
loadInitialData() {
|
|
||||||
return store.dispatch(storeActions.GET_VEHICLE_YEARS, {});
|
|
||||||
},
|
|
||||||
initializeComponent(cmsContent, initialData) {
|
|
||||||
this.questionText = cmsContent.QuestionText;
|
|
||||||
this.years = initialData;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
selectedYear(val) {
|
|
||||||
this.$emit('update:modelValue', val)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
modelValue: String,
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
radioQuestion,
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
loadInitialData() {
|
||||||
|
return store.dispatch(storeActions.GET_VEHICLE_YEARS, {});
|
||||||
|
},
|
||||||
|
initializeComponent(cmsContent, initialData) {
|
||||||
|
this.questionText = cmsContent.QuestionText;
|
||||||
|
this.years = initialData;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
selectedYear(val) {
|
||||||
|
this.$emit("update:modelValue", val);
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,9 @@ import { storeMutations } from "@/constants/store-mutations.js";
|
||||||
import { widgetNames } from "@/constants/widget-names.js";
|
import { widgetNames } from "@/constants/widget-names.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data(){
|
data() {
|
||||||
return {
|
return {
|
||||||
isCmsContentReady: false
|
};
|
||||||
}
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// dispatchBlockingStoreAction(type, payload) {
|
// dispatchBlockingStoreAction(type, payload) {
|
||||||
|
|
|
||||||
|
|
@ -1,54 +1,53 @@
|
||||||
import baseMixin from "@/mixins/base-mixin"
|
import baseMixin from "@/mixins/base-mixin";
|
||||||
import { storeActions } from "@/constants/store-actions.js";
|
import { storeActions } from "@/constants/store-actions.js";
|
||||||
import { widgetNames } from "@/constants/widget-names.js";
|
import { widgetNames } from "@/constants/widget-names.js";
|
||||||
|
|
||||||
describe("baseMixin.js", () => {
|
describe("baseMixin.js", () => {
|
||||||
test('dispatchNonblockingStoreAction: calls dispatch with type and payload', () => {
|
test("dispatchNonblockingStoreAction: calls dispatch with type and payload", () => {
|
||||||
const mixIn = getMixInInstance({});
|
const mixIn = getMixInInstance({});
|
||||||
const type = {};
|
const type = {};
|
||||||
const payload = {};
|
const payload = {};
|
||||||
|
|
||||||
mixIn.methods.dispatchNonBlockingStoreAction(type, payload);
|
mixIn.methods.dispatchNonBlockingStoreAction(type, payload);
|
||||||
|
|
||||||
expect(mixIn.methods.$store.dispatch).toBeCalledWith(type, payload);
|
expect(mixIn.methods.$store.dispatch).toBeCalledWith(type, payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('dispatchNonblockingStoreAction: calls dispatch with type and payload, handles Uri encode', () => {
|
test("dispatchNonblockingStoreAction: calls dispatch with type and payload, handles Uri encode", () => {
|
||||||
const mixIn = getMixInInstance({});
|
const mixIn = getMixInInstance({});
|
||||||
const type = {};
|
const type = {};
|
||||||
const payload = { make: 'Alfa Romeo/Chrysler' };
|
const payload = { make: "Alfa Romeo/Chrysler" };
|
||||||
|
|
||||||
mixIn.methods.dispatchNonBlockingStoreAction(type, payload, true);
|
mixIn.methods.dispatchNonBlockingStoreAction(type, payload, true);
|
||||||
|
|
||||||
expect(mixIn.methods.$store.dispatch).toBeCalledWith(type, payload);
|
expect(mixIn.methods.$store.dispatch).toBeCalledWith(type, payload);
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
function getMixInInstance({ isDispatchSuccess = true }) {
|
function getMixInInstance({ isDispatchSuccess = true }) {
|
||||||
|
// Mock Store
|
||||||
|
const store = {
|
||||||
|
dispatch: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
// Mock Store
|
if (isDispatchSuccess) {
|
||||||
const store = {
|
store.dispatch.mockReturnValue(Promise.resolve());
|
||||||
dispatch: jest.fn()
|
} else {
|
||||||
}
|
store.dispatch.mockReturnValue(Promise.reject());
|
||||||
|
}
|
||||||
|
|
||||||
if (isDispatchSuccess) {
|
// Mock Route
|
||||||
store.dispatch.mockReturnValue(Promise.resolve())
|
const route = {
|
||||||
} else {
|
query: {
|
||||||
store.dispatch.mockReturnValue(Promise.reject())
|
fmgPage: "test-page",
|
||||||
}
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// Mock Route
|
// Attach mocks to mixin
|
||||||
const route = {
|
const baseMixIn = baseMixin;
|
||||||
query: {
|
baseMixIn.methods.$route = route;
|
||||||
fmgPage: 'test-page'
|
baseMixIn.methods.$store = store;
|
||||||
}
|
baseMixIn.methods.storeActions = storeActions;
|
||||||
};
|
baseMixIn.methods.widgetNames = widgetNames;
|
||||||
|
|
||||||
// Attach mocks to mixin
|
return baseMixIn;
|
||||||
const baseMixIn = baseMixin;
|
}
|
||||||
baseMixIn.methods.$route = route;
|
|
||||||
baseMixIn.methods.$store = store;
|
|
||||||
baseMixIn.methods.storeActions = storeActions;
|
|
||||||
baseMixIn.methods.widgetNames = widgetNames;
|
|
||||||
|
|
||||||
return baseMixIn;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { createWebHistory, createRouter } from "vue-router";
|
import { createWebHistory, createRouter } from "vue-router";
|
||||||
import { storeActions } from "@/constants/store-actions.js";
|
import { storeActions } from "@/constants/store-actions.js";
|
||||||
import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js";
|
import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js";
|
||||||
|
import { routingTable } from "@/router/router-constants/routing-table.js";
|
||||||
import ComponentTest from "@/layouts/component-test/component-test.vue";
|
import ComponentTest from "@/layouts/component-test/component-test.vue";
|
||||||
import LoaderDemo from "@/layouts/loader-demo/loader-demo.vue";
|
import LoaderDemo from "@/layouts/loader-demo/loader-demo.vue";
|
||||||
import NotFound from "@/layouts/not-found/not-found.vue";
|
import NotFound from "@/layouts/not-found/not-found.vue";
|
||||||
|
|
@ -70,6 +71,59 @@ const router = createRouter({
|
||||||
routes,
|
routes,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//---------------------------------------------------------- Router Functions ----------------------------------------------------------
|
||||||
|
|
||||||
|
// Navigate to the next route, depending on the scenario.
|
||||||
|
router.navigate = (
|
||||||
|
scenario,
|
||||||
|
currentRoute,
|
||||||
|
optionalQuery = {},
|
||||||
|
optionalParams = {}
|
||||||
|
) => {
|
||||||
|
if (!scenario) {
|
||||||
|
console.error("No scenario provided. Please review the routing table.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match our maps up and navigate if we have a destination.
|
||||||
|
const matchingScenarioMap = router.getNavigationMap(scenario, currentRoute);
|
||||||
|
|
||||||
|
if (matchingScenarioMap.destinationFmgPageValue !== undefined) {
|
||||||
|
// We're always pushing the same path, just changing query strings. Make sure our optional query strings get combined with our fmgPage one.
|
||||||
|
router.push({
|
||||||
|
path: "/",
|
||||||
|
query: Object.assign(optionalQuery, {
|
||||||
|
fmgPage: matchingScenarioMap.destinationFmgPageValue,
|
||||||
|
}),
|
||||||
|
params: optionalParams,
|
||||||
|
});
|
||||||
|
} else if (matchingScenarioMap.destinationUrl !== undefined) {
|
||||||
|
navigateToUrl(matchingScenarioMap.destinationUrl);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get navigation map depeding on the scenario and the current 'page' you're on.
|
||||||
|
router.getNavigationMap = (scenario, currentRoute) => {
|
||||||
|
const fmgPageValue = currentRoute.query.fmgPage;
|
||||||
|
const matchedQueryValue = routingTable
|
||||||
|
.filter(
|
||||||
|
(item) =>
|
||||||
|
item.fmgPageValue === fmgPageValue &&
|
||||||
|
item.maps.filter((map) => map.scenario === scenario).length > 0
|
||||||
|
)
|
||||||
|
.map((m) => m.maps.filter((map) => map.scenario === scenario));
|
||||||
|
|
||||||
|
return matchedQueryValue[0][0];
|
||||||
|
};
|
||||||
|
|
||||||
|
//---------------------------------------------------------- Private Functions ----------------------------------------------------------
|
||||||
|
|
||||||
|
// Navigate to an external url.
|
||||||
|
function navigateToUrl(url) {
|
||||||
|
// possibly show some loading screen in the future here.
|
||||||
|
window.location.assign(url);
|
||||||
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
function GetRouteInfoFromPageName(pageName) {
|
function GetRouteInfoFromPageName(pageName) {
|
||||||
|
|
|
||||||
6
src/router/router-constants/fmgPage-values.js
Normal file
6
src/router/router-constants/fmgPage-values.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
const fmgPageValues = {
|
||||||
|
VEHICLE_YEAR: "vehicle-year",
|
||||||
|
VEHICLE_MAKE: "vehicle-make",
|
||||||
|
};
|
||||||
|
|
||||||
|
export { fmgPageValues };
|
||||||
5
src/router/router-constants/navigation-scenarios.js
Normal file
5
src/router/router-constants/navigation-scenarios.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
const navigationScenarios = {
|
||||||
|
SELECTED_YEAR: "SELECTED_YEAR",
|
||||||
|
};
|
||||||
|
|
||||||
|
export { navigationScenarios };
|
||||||
16
src/router/router-constants/routing-table.js
Normal file
16
src/router/router-constants/routing-table.js
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
|
||||||
|
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
|
||||||
|
|
||||||
|
const routingTable = [
|
||||||
|
{
|
||||||
|
fmgPageValue: fmgPageValues.VEHICLE_YEAR,
|
||||||
|
maps: [
|
||||||
|
{
|
||||||
|
scenario: navigationScenarios.SELECTED_YEAR,
|
||||||
|
destinationFmgPageValue: fmgPageValues.VEHICLE_MAKE,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export { routingTable };
|
||||||
|
|
@ -32,7 +32,7 @@ export default createStore({
|
||||||
zipCode: null,
|
zipCode: null,
|
||||||
firstName: null,
|
firstName: null,
|
||||||
lastName: null,
|
lastName: null,
|
||||||
licensePlate: null
|
licensePlate: null,
|
||||||
},
|
},
|
||||||
damage: {
|
damage: {
|
||||||
isRepair: null,
|
isRepair: null,
|
||||||
|
|
@ -52,16 +52,16 @@ export default createStore({
|
||||||
isCash: null,
|
isCash: null,
|
||||||
},
|
},
|
||||||
referralSeqNum: null,
|
referralSeqNum: null,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
applicationUser: {
|
applicationUser: {
|
||||||
experiments: null,
|
experiments: null,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
// See IMPORTANT note at top of "state" declaration.
|
// See IMPORTANT note at top of "state" declaration.
|
||||||
|
|
||||||
mutations: {
|
mutations: {
|
||||||
updateYear(state, year){
|
updateYear(state, year) {
|
||||||
state.order.vehicle.year = year;
|
state.order.vehicle.year = year;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -86,7 +86,7 @@ export default createStore({
|
||||||
method: endpoints.LookupVehicleByVin.method,
|
method: endpoints.LookupVehicleByVin.method,
|
||||||
endpoint: endpoints.LookupVehicleByVin.url,
|
endpoint: endpoints.LookupVehicleByVin.url,
|
||||||
payload: {
|
payload: {
|
||||||
"vin": vin // EX "1J4GW58S4XC541166"
|
vin: vin, // EX "1J4GW58S4XC541166"
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
@ -135,6 +135,6 @@ export default createStore({
|
||||||
endpoint: relativeUrl,
|
endpoint: relativeUrl,
|
||||||
payload: {},
|
payload: {},
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,121 +1,131 @@
|
||||||
import store from './index'
|
import store from "./index";
|
||||||
import globalMethods from '@/global-methods'
|
import globalMethods from "@/global-methods";
|
||||||
|
|
||||||
describe("Actions", () => {
|
describe("Actions", () => {
|
||||||
it("Should return list of years retrieved", async () => {
|
it("Should return list of years retrieved", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
let years = [];
|
let years = [];
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
globalMethods.callHttpClient = jest.fn();
|
globalMethods.callHttpClient = jest.fn();
|
||||||
globalMethods.callHttpClient.mockImplementation(() => {
|
globalMethods.callHttpClient.mockImplementation(() => {
|
||||||
return Promise.resolve({ data: [2023,2022,2021]});
|
return Promise.resolve({ data: [2023, 2022, 2021] });
|
||||||
});
|
});
|
||||||
await store.dispatch('getVehicleYears')
|
await store.dispatch("getVehicleYears").then((response) => {
|
||||||
.then( (response) => {
|
years = response.data;
|
||||||
years = response.data;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(years[0]).toBe(2023);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should return list of makes retrieved", async () => {
|
// Assert
|
||||||
// Arrange
|
expect(years[0]).toBe(2023);
|
||||||
let makes = [];
|
});
|
||||||
|
|
||||||
// Act
|
it("Should return list of makes retrieved", async () => {
|
||||||
globalMethods.callHttpClient.mockImplementation(() => {
|
// Arrange
|
||||||
return Promise.resolve({ data: ['Baic','Honda','Ford']});
|
let makes = [];
|
||||||
});
|
|
||||||
await store.dispatch('getVehicleMakes', {year: 2023})
|
|
||||||
.then( (response) => {
|
|
||||||
makes = response.data;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
// Act
|
||||||
expect(makes[0]).toBe('Baic');
|
globalMethods.callHttpClient.mockImplementation(() => {
|
||||||
|
return Promise.resolve({ data: ["Baic", "Honda", "Ford"] });
|
||||||
|
});
|
||||||
|
await store.dispatch("getVehicleMakes", { year: 2023 }).then((response) => {
|
||||||
|
makes = response.data;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should return list of models retrieved", async () => {
|
// Assert
|
||||||
// Arrange
|
expect(makes[0]).toBe("Baic");
|
||||||
let models = [];
|
});
|
||||||
|
|
||||||
// Act
|
it("Should return list of models retrieved", async () => {
|
||||||
globalMethods.callHttpClient.mockImplementation(() => {
|
// Arrange
|
||||||
return Promise.resolve({ data: ['BJ40 (MEX)','Civic','Accord']});
|
let models = [];
|
||||||
});
|
|
||||||
await store.dispatch('getVehicleModels', {year: 2023, make: 'Baic'})
|
|
||||||
.then( (response) => {
|
|
||||||
models = response.data;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
// Act
|
||||||
expect(models[0]).toBe('BJ40 (MEX)');
|
globalMethods.callHttpClient.mockImplementation(() => {
|
||||||
|
return Promise.resolve({ data: ["BJ40 (MEX)", "Civic", "Accord"] });
|
||||||
});
|
});
|
||||||
|
await store
|
||||||
|
.dispatch("getVehicleModels", { year: 2023, make: "Baic" })
|
||||||
|
.then((response) => {
|
||||||
|
models = response.data;
|
||||||
|
});
|
||||||
|
|
||||||
it("Should return list of styles retrieved", async () => {
|
// Assert
|
||||||
// Arrange
|
expect(models[0]).toBe("BJ40 (MEX)");
|
||||||
let styles = [];
|
});
|
||||||
|
|
||||||
// Act
|
it("Should return list of styles retrieved", async () => {
|
||||||
globalMethods.callHttpClient.mockImplementation(() => {
|
// Arrange
|
||||||
return Promise.resolve({ data: ['4 DOOR UTILITY','2 DOOR']});
|
let styles = [];
|
||||||
});
|
|
||||||
await store.dispatch('getVehicleStyles', {year: 2023, make: 'Baic', model: 'BJ40 (MEX)'})
|
|
||||||
.then( (response) => {
|
|
||||||
styles = response.data;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
// Act
|
||||||
expect(styles[0]).toBe('4 DOOR UTILITY');
|
globalMethods.callHttpClient.mockImplementation(() => {
|
||||||
|
return Promise.resolve({ data: ["4 DOOR UTILITY", "2 DOOR"] });
|
||||||
});
|
});
|
||||||
|
await store
|
||||||
|
.dispatch("getVehicleStyles", {
|
||||||
|
year: 2023,
|
||||||
|
make: "Baic",
|
||||||
|
model: "BJ40 (MEX)",
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
styles = response.data;
|
||||||
|
});
|
||||||
|
|
||||||
it("Should return data from url retrieved", async () => {
|
// Assert
|
||||||
// Arrange
|
expect(styles[0]).toBe("4 DOOR UTILITY");
|
||||||
let routeInfo = [];
|
});
|
||||||
|
|
||||||
// Act
|
it("Should return data from url retrieved", async () => {
|
||||||
globalMethods.callHttpClient.mockImplementation(() => {
|
// Arrange
|
||||||
return Promise.resolve({ data: {
|
let routeInfo = [];
|
||||||
Result: 'Route Info Data'
|
|
||||||
}});
|
|
||||||
});
|
|
||||||
await store.dispatch('getRouteInfo', { pageName: 'vehicle-year' })
|
|
||||||
.then( (response) => {
|
|
||||||
routeInfo = response.data.Result;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
// Act
|
||||||
expect(routeInfo).toBe('Route Info Data');
|
globalMethods.callHttpClient.mockImplementation(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
data: {
|
||||||
|
Result: "Route Info Data",
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
await store
|
||||||
|
.dispatch("getRouteInfo", { pageName: "vehicle-year" })
|
||||||
|
.then((response) => {
|
||||||
|
routeInfo = response.data.Result;
|
||||||
|
});
|
||||||
|
|
||||||
it("Should return page data from url retrieved", async () => {
|
// Assert
|
||||||
// Arrange
|
expect(routeInfo).toBe("Route Info Data");
|
||||||
let pageData = [];
|
});
|
||||||
|
|
||||||
// Act
|
it("Should return page data from url retrieved", async () => {
|
||||||
globalMethods.callHttpClient.mockImplementation(() => {
|
// Arrange
|
||||||
return Promise.resolve({ data: {
|
let pageData = [];
|
||||||
Result: 'Page Info Data'
|
|
||||||
}});
|
|
||||||
});
|
|
||||||
await store.dispatch('getPageData', { pageName: 'vehicle-year' })
|
|
||||||
.then( (response) => {
|
|
||||||
pageData = response.data.Result;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
// Act
|
||||||
expect(pageData).toBe('Page Info Data');
|
globalMethods.callHttpClient.mockImplementation(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
data: {
|
||||||
|
Result: "Page Info Data",
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
})
|
await store
|
||||||
|
.dispatch("getPageData", { pageName: "vehicle-year" })
|
||||||
|
.then((response) => {
|
||||||
|
pageData = response.data.Result;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(pageData).toBe("Page Info Data");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("Mutations", () => {
|
describe("Mutations", () => {
|
||||||
it("Should update the year property in the store", () => {
|
it("Should update the year property in the store", () => {
|
||||||
// Act
|
// Act
|
||||||
store.commit('updateYear', 2020);
|
store.commit("updateYear", 2020);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(store.state.order.vehicle.year).toBe(2020);
|
expect(store.state.order.vehicle.year).toBe(2020);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -25,5 +25,14 @@ body {
|
||||||
.container-fluid {
|
.container-fluid {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
left: -10000px;
|
||||||
|
top: auto;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Hide horizontal scrollbar
|
// Hide horizontal scrollbar
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ $gray-200: #E3E4E4;// Used in theme
|
||||||
$gray-300: #D2D4D4;
|
$gray-300: #D2D4D4;
|
||||||
$gray: #B0B3B3;// Default Gray
|
$gray: #B0B3B3;// Default Gray
|
||||||
$gray-500: #8E9292;// Used in theme
|
$gray-500: #8E9292;// Used in theme
|
||||||
|
$gray-550: #727676;// Used in theme
|
||||||
$gray-600: #4D5151;// Used in theme
|
$gray-600: #4D5151;// Used in theme
|
||||||
$gray-700: #303333;// Used in theme
|
$gray-700: #303333;// Used in theme
|
||||||
$gray-800: #222424;// Used in theme
|
$gray-800: #222424;// Used in theme
|
||||||
|
|
@ -130,6 +131,7 @@ $h5-font-size: $font-size-base * 1.25;
|
||||||
$h6-font-size: $font-size-base * .875;
|
$h6-font-size: $font-size-base * .875;
|
||||||
|
|
||||||
//Border Radius
|
//Border Radius
|
||||||
|
// Helper classes are rounded, rounded-1, rounded-2, rounded-3
|
||||||
$border-radius: .25rem;
|
$border-radius: .25rem;
|
||||||
$border-radius-sm: .2rem;
|
$border-radius-sm: .2rem;
|
||||||
$border-radius-lg: .5rem;//Used for buttons. Can be used for other things, of course.
|
$border-radius-lg: .5rem;//Used for buttons. Can be used for other things, of course.
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
test.todo('some test to be written in the future');
|
test.todo("some test to be written in the future");
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,25 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="alert fade show text-center mb-0 py-2 px-3" role="alert"
|
<div
|
||||||
:class="[ isDismissible ? 'alert-dismissible' : '', this.alertClass ]"
|
class="alert fade show text-center mb-0 py-2 px-3"
|
||||||
|
role="alert"
|
||||||
|
:class="[isDismissible ? 'alert-dismissible' : '', this.alertClass]"
|
||||||
|
>
|
||||||
|
<p class="m-0 fw-bold small alert-heading">{{ alertHeadline }}</p>
|
||||||
|
<p class="m-0 text-body small">{{ alertCopy }}</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-close p-2"
|
||||||
|
data-bs-dismiss="alert"
|
||||||
|
aria-label="Close"
|
||||||
>
|
>
|
||||||
<p class="m-0 fw-bold small alert-heading">{{alertHeadline}}</p>
|
<svg
|
||||||
<p class="m-0 text-body small">{{alertCopy}}</p>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<button type="button" class="btn-close p-2" data-bs-dismiss="alert" aria-label="Close">
|
viewBox="0 0 23.7 23.7"
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23.7 23.7" xml:space="preserve">
|
xml:space="preserve"
|
||||||
<path d="m23.24 2.7-9.15 9.15L23.24 21a1.581 1.581 0 0 1-1.12 2.7c-.42 0-.82-.16-1.12-.46l-9.15-9.15-9.15 9.15c-.3.3-.7.46-1.12.46A1.581 1.581 0 0 1 .46 21l8.47-8.47.68-.68L.46 2.7c-.62-.62-.62-1.62 0-2.24.62-.62 1.62-.62 2.24 0l8.47 8.47.68.68L21 .46a1.57 1.57 0 0 1 2.23 0c.63.62.63 1.62.01 2.24z"/>
|
>
|
||||||
|
<path
|
||||||
|
d="m23.24 2.7-9.15 9.15L23.24 21a1.581 1.581 0 0 1-1.12 2.7c-.42 0-.82-.16-1.12-.46l-9.15-9.15-9.15 9.15c-.3.3-.7.46-1.12.46A1.581 1.581 0 0 1 .46 21l8.47-8.47.68-.68L.46 2.7c-.62-.62-.62-1.62 0-2.24.62-.62 1.62-.62 2.24 0l8.47 8.47.68.68L21 .46a1.57 1.57 0 0 1 2.23 0c.63.62.63 1.62.01 2.24z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -26,8 +39,8 @@ export default {
|
||||||
alert-warning (yellow)
|
alert-warning (yellow)
|
||||||
alert-info (blue)
|
alert-info (blue)
|
||||||
*/
|
*/
|
||||||
alertClass: String
|
alertClass: String,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -39,8 +52,8 @@ export default {
|
||||||
.btn-close {
|
.btn-close {
|
||||||
background: none;
|
background: none;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
width: .75rem;
|
width: 0.75rem;
|
||||||
height: .75rem;
|
height: 0.75rem;
|
||||||
}
|
}
|
||||||
&.alert-dismissible {
|
&.alert-dismissible {
|
||||||
button {
|
button {
|
||||||
|
|
|
||||||
1
src/ux-components/button-back/button-back.spec.js
Normal file
1
src/ux-components/button-back/button-back.spec.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
test.todo("some test to be written in the future");
|
||||||
65
src/ux-components/button-back/button-back.vue
Normal file
65
src/ux-components/button-back/button-back.vue
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
<template>
|
||||||
|
<button class="back-button-wrapper d-flex p-0">
|
||||||
|
<div class="button-back">
|
||||||
|
<div class="arrow-left"></div>
|
||||||
|
<div class="box"></div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "buttonBack"
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.back-button-wrapper {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
.button-back {
|
||||||
|
display: inline-flex;
|
||||||
|
position: relative;
|
||||||
|
height: 18px;
|
||||||
|
width: 23px;
|
||||||
|
overflow: hidden;
|
||||||
|
.arrow-left {
|
||||||
|
position: absolute;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
left: 4px;
|
||||||
|
background-color: $gray-550;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.box {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
background: $gray-550;
|
||||||
|
height: 100%;
|
||||||
|
width: 11px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
left: 12px;
|
||||||
|
border-radius: 5px;
|
||||||
|
&:before, &:after {
|
||||||
|
position: absolute;
|
||||||
|
left: 1px;
|
||||||
|
top: 3px;
|
||||||
|
content: " ";
|
||||||
|
height: 12px;
|
||||||
|
width: 2px;
|
||||||
|
background-color: $white;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
&:before {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
&:after {
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -3,14 +3,14 @@
|
||||||
:disabled="isDisabled"
|
:disabled="isDisabled"
|
||||||
:aria-disabled="isDisabled"
|
:aria-disabled="isDisabled"
|
||||||
class="btn btn-primary d-flex align-items-center py-3 px-4"
|
class="btn btn-primary d-flex align-items-center py-3 px-4"
|
||||||
@click='displayComponent'
|
@click="displayComponent"
|
||||||
>
|
>
|
||||||
<span class="m-0">{{ this.buttonText }}</span>
|
<span class="m-0">{{ this.buttonText }}</span>
|
||||||
<loader
|
<loader
|
||||||
class="ms-2"
|
class="ms-2"
|
||||||
v-if="display"
|
v-if="display"
|
||||||
v-bind:style="{width: `${sizeInRem}rem`, height: `${sizeInRem}rem`}"
|
v-bind:style="{ width: `${sizeInRem}rem`, height: `${sizeInRem}rem` }"
|
||||||
v-bind:class="[this.loaderColor, this.loaderPosition]"
|
v-bind:class="[this.loaderColor, this.loaderPosition]"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -24,7 +24,7 @@ export default {
|
||||||
isDisabled: Boolean,
|
isDisabled: Boolean,
|
||||||
loaderColor: String,
|
loaderColor: String,
|
||||||
loaderPosition: String,
|
loaderPosition: String,
|
||||||
sizeInRem: [Number,String]
|
sizeInRem: [Number, String],
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,14 @@
|
||||||
:disabled="isDisabled"
|
:disabled="isDisabled"
|
||||||
:aria-disabled="isDisabled"
|
:aria-disabled="isDisabled"
|
||||||
class="btn btn-secondary d-flex align-items-center py-3 px-4"
|
class="btn btn-secondary d-flex align-items-center py-3 px-4"
|
||||||
@click='displayComponent'
|
@click="displayComponent"
|
||||||
>
|
>
|
||||||
<span class="m-0">{{ this.buttonText }}</span>
|
<span class="m-0">{{ this.buttonText }}</span>
|
||||||
<loader
|
<loader
|
||||||
class="ms-2"
|
class="ms-2"
|
||||||
v-if="display"
|
v-if="display"
|
||||||
v-bind:style="{width: `${sizeInRem}rem`, height: `${sizeInRem}rem`}"
|
v-bind:style="{ width: `${sizeInRem}rem`, height: `${sizeInRem}rem` }"
|
||||||
v-bind:class="[this.loaderColor, this.loaderPosition]"
|
v-bind:class="[this.loaderColor, this.loaderPosition]"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -24,7 +24,7 @@ export default {
|
||||||
isDisabled: Boolean,
|
isDisabled: Boolean,
|
||||||
loaderColor: String,
|
loaderColor: String,
|
||||||
loaderPosition: String,
|
loaderPosition: String,
|
||||||
sizeInRem: [Number,String]
|
sizeInRem: [Number, String],
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="current_car_info-text">
|
<div class="current_car_info-text">
|
||||||
<div class="d-flex align-items-center justify-content-center">
|
<div class="d-flex align-items-center justify-content-center">
|
||||||
<h2 class="text-center text-dark fs-5 d-block fw-normal mb-0">{{ text }}</h2>
|
<h2 class="text-center fs-5 d-block fw-normal mb-0">{{ text }}</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -14,3 +14,9 @@ export default {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
h2 {
|
||||||
|
color: $gray-550;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,27 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="d-flex flex-column w-100">
|
<div class="d-flex flex-column w-100">
|
||||||
<button
|
<button
|
||||||
class="btn list-button d-flex align-items-center justify-content-between py-3 px-4"
|
class="
|
||||||
@click='displayComponent'
|
btn
|
||||||
v-bind:class="[
|
list-button
|
||||||
this.isError ? 'error' : '',
|
d-flex
|
||||||
]"
|
align-items-center
|
||||||
|
justify-content-between
|
||||||
|
py-3
|
||||||
|
px-4
|
||||||
|
"
|
||||||
|
@click="displayComponent"
|
||||||
|
v-bind:class="[this.isError ? 'error' : '']"
|
||||||
>
|
>
|
||||||
<span class="m-0">{{ this.buttonText }}</span>
|
<span class="m-0">{{ this.buttonText }}</span>
|
||||||
<loader
|
<loader
|
||||||
v-if="display"
|
v-if="display"
|
||||||
v-bind:style="{width: `${sizeInRem}rem`, height: `${sizeInRem}rem`}"
|
v-bind:style="{ width: `${sizeInRem}rem`, height: `${sizeInRem}rem` }"
|
||||||
v-bind:class="[this.loaderColor, this.loaderPosition]"
|
v-bind:class="[this.loaderColor, this.loaderPosition]"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<label class="small mt-1">{{ this.errorText }}</label>
|
<label class="small mt-1">{{ this.errorText }}</label>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -27,12 +33,12 @@ export default {
|
||||||
errorText: String,
|
errorText: String,
|
||||||
loaderColor: String,
|
loaderColor: String,
|
||||||
loaderPosition: String,
|
loaderPosition: String,
|
||||||
sizeInRem: [Number,String]
|
sizeInRem: [Number, String],
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
display: false,
|
display: false,
|
||||||
isError: false
|
isError: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="loader"
|
<div class="loader"
|
||||||
|
role="alert"
|
||||||
|
aria-label="Loading new page"
|
||||||
v-bind:style="{width: `${sizeInRem}rem`, height: `${sizeInRem}rem`}"
|
v-bind:style="{width: `${sizeInRem}rem`, height: `${sizeInRem}rem`}"
|
||||||
v-bind:class="[this.loaderColor, this.loaderPosition]"
|
v-bind:class="[this.loaderColor, this.loaderPosition]"
|
||||||
></div>
|
></div>
|
||||||
|
|
@ -12,18 +14,18 @@ export default {
|
||||||
props: {
|
props: {
|
||||||
sizeInRem: {
|
sizeInRem: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 1
|
default: 1,
|
||||||
},
|
},
|
||||||
/* Color options: red, green, blue, white, black */
|
/* Color options: red, green, blue, white, black */
|
||||||
loaderColor: {
|
loaderColor: {
|
||||||
type: String
|
type: String,
|
||||||
},
|
},
|
||||||
/* Position options: center, right, left (OPTIONAL, do NOT use on btn-* classes) */
|
/* Position options: center, right, left (OPTIONAL, do NOT use on btn-* classes) */
|
||||||
loaderPosition: {
|
loaderPosition: {
|
||||||
type: String
|
type: String,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
@ -31,7 +33,7 @@ export default {
|
||||||
display: flex;
|
display: flex;
|
||||||
//Open an overlay to prevent page interaction
|
//Open an overlay to prevent page interaction
|
||||||
&:before {
|
&:before {
|
||||||
content: '';
|
content: "";
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|
@ -43,7 +45,7 @@ export default {
|
||||||
}
|
}
|
||||||
//Spinner basics
|
//Spinner basics
|
||||||
&:after {
|
&:after {
|
||||||
content: '';
|
content: "";
|
||||||
mask: url(../../assets/img/icons/spinner.svg);
|
mask: url(../../assets/img/icons/spinner.svg);
|
||||||
mask-size: cover;
|
mask-size: cover;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
@ -52,7 +54,7 @@ export default {
|
||||||
animation: rotation 1s infinite linear;
|
animation: rotation 1s infinite linear;
|
||||||
@keyframes rotation {
|
@keyframes rotation {
|
||||||
100% {
|
100% {
|
||||||
transform:rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -70,7 +72,8 @@ export default {
|
||||||
left: 1rem;
|
left: 1rem;
|
||||||
}
|
}
|
||||||
//Spinner color
|
//Spinner color
|
||||||
&:after { //Default spinner color (blue) if no other color is specified from the options below
|
&:after {
|
||||||
|
//Default spinner color (blue) if no other color is specified from the options below
|
||||||
background-color: $blue;
|
background-color: $blue;
|
||||||
}
|
}
|
||||||
&.red:after {
|
&.red:after {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ describe("radio.vue", () => {
|
||||||
loaderColor: "blue",
|
loaderColor: "blue",
|
||||||
loaderPosition: "right",
|
loaderPosition: "right",
|
||||||
sizeInRem: "1.5",
|
sizeInRem: "1.5",
|
||||||
errorMessage: 'null'
|
errorMessage: "null",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -20,14 +20,14 @@ describe("radio.vue", () => {
|
||||||
const label = wrapper.find("label");
|
const label = wrapper.find("label");
|
||||||
const paragraph = wrapper.find("span");
|
const paragraph = wrapper.find("span");
|
||||||
|
|
||||||
await label.trigger('click');
|
await label.trigger("click");
|
||||||
|
|
||||||
expect(input.attributes()).toEqual({
|
expect(input.attributes()).toEqual({
|
||||||
id: "2023",
|
id: "2023",
|
||||||
type: "radio",
|
type: "radio",
|
||||||
value: "2023",
|
value: "2023",
|
||||||
name: "TestGroup",
|
name: "TestGroup",
|
||||||
"aria-required": "true"
|
"aria-required": "true",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(label.attributes()).toEqual({
|
expect(label.attributes()).toEqual({
|
||||||
|
|
@ -35,14 +35,13 @@ describe("radio.vue", () => {
|
||||||
tabindex: "-1",
|
tabindex: "-1",
|
||||||
for: "2023",
|
for: "2023",
|
||||||
class: "d-flex flex-column justify-content-center py-3 px-4 last-item",
|
class: "d-flex flex-column justify-content-center py-3 px-4 last-item",
|
||||||
"aria-checked": "false"
|
"aria-checked": "false",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(label.text()).toEqual('2023');
|
expect(label.text()).toEqual("2023");
|
||||||
|
|
||||||
expect(paragraph.text()).toEqual('2023');
|
expect(paragraph.text()).toEqual("2023");
|
||||||
|
|
||||||
expect(wrapper.vm.display).toBe(true);
|
expect(wrapper.vm.display).toBe(true);
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,4 @@
|
||||||
<!-- See the component-test.vue page for example implementation -->
|
<!-- See the component-test.vue page for example implementation -->
|
||||||
<!-- role="radiogroup" and aria-labelledby must be included in the parent component for the radio group -->
|
|
||||||
<!-- Example: -->
|
|
||||||
<!-- <div role="radiogroup" aria-labelledby="demo-radio-group" class="col my-3 d-flex align-items-center flex-column"> -->
|
|
||||||
<!-- An h3 with id must be included just before the opening radio button group. ***The id must match the aria-labelledby of the parent div.*** -->
|
|
||||||
<!-- Example -->
|
|
||||||
<!-- <h3 class="visually-hidden" id="demo-radio-group">Select Vehicle Year</h3> -->
|
|
||||||
<template>
|
<template>
|
||||||
<!-- IMPORTANT: Refrain from using more than 4 horizontal radio buttons on desktop, 3 on mobile. -->
|
<!-- IMPORTANT: Refrain from using more than 4 horizontal radio buttons on desktop, 3 on mobile. -->
|
||||||
<div class="col radiogroup radio-horizontal d-flex flex-column mb-2">
|
<div class="col radiogroup radio-horizontal d-flex flex-column mb-2">
|
||||||
|
|
@ -12,9 +6,10 @@
|
||||||
<label role="radio" tabindex="-1" aria-checked="false" :for="radioID" class="d-flex flex-column justify-content-center py-3 px-4" :class="isFirstOrLastButton" @click='displayComponent'>
|
<label role="radio" tabindex="-1" aria-checked="false" :for="radioID" class="d-flex flex-column justify-content-center py-3 px-4" :class="isFirstOrLastButton" @click='displayComponent'>
|
||||||
<span class="m-0" :class="[this.textPosition]">{{radioID}}</span>
|
<span class="m-0" :class="[this.textPosition]">{{radioID}}</span>
|
||||||
<span class="m-0 small" :class="[this.textPosition]">{{radioLabelSubCopy}}</span>
|
<span class="m-0 small" :class="[this.textPosition]">{{radioLabelSubCopy}}</span>
|
||||||
|
<span v-if="screenReaderOnlyText" class="sr-only">{{screenReaderOnlyText}}</span>
|
||||||
<loader v-if="display" :style="{width: `${sizeInRem}rem`, height: `${sizeInRem}rem`}" :class="[this.loaderColor, this.loaderPosition]" />
|
<loader v-if="display" :style="{width: `${sizeInRem}rem`, height: `${sizeInRem}rem`}" :class="[this.loaderColor, this.loaderPosition]" />
|
||||||
</label>
|
</label>
|
||||||
<p class="small">{{errorMessage}}</p>
|
<p class="small">{{ errorMessage }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -26,6 +21,7 @@ export default {
|
||||||
groupName: String, /* Required, unique for each radio button GROUP */
|
groupName: String, /* Required, unique for each radio button GROUP */
|
||||||
radioID: String, /* Required, unique for each radio button. Used for button id, label and <label for> */
|
radioID: String, /* Required, unique for each radio button. Used for button id, label and <label for> */
|
||||||
radioLabelSubCopy: String, /* Optional, used for multi-line radio buttons */
|
radioLabelSubCopy: String, /* Optional, used for multi-line radio buttons */
|
||||||
|
screenReaderOnlyText: String, /* Optional, copy to be read by screenreader */
|
||||||
textPosition: String, /* Optional, use Bootstrap classes: text-start, text-center, text-end. Default (empty) is text-start */
|
textPosition: String, /* Optional, use Bootstrap classes: text-start, text-center, text-end. Default (empty) is text-start */
|
||||||
errorMessage: String, /* Optional, if there is an error message to be displayed */
|
errorMessage: String, /* Optional, if there is an error message to be displayed */
|
||||||
loaderColor: String, /* Specify color of loader/spinner. Options are blue, red, green, white, black. Default is blue */
|
loaderColor: String, /* Specify color of loader/spinner. Options are blue, red, green, white, black. Default is blue */
|
||||||
|
|
@ -37,28 +33,28 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isError: false,
|
isError: false,
|
||||||
display: false
|
display: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
displayComponent() {
|
displayComponent() {
|
||||||
this.display = true;
|
this.display = true;
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
loader,
|
loader,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isFirstOrLastButton() {
|
isFirstOrLastButton() {
|
||||||
let className = '';
|
let className = "";
|
||||||
if(this.positionInGroup == this.totalInGroup) {
|
if (this.positionInGroup == this.totalInGroup) {
|
||||||
className = 'last-item'
|
className = "last-item";
|
||||||
} else if(this.positionInGroup == 1) {
|
} else if (this.positionInGroup == 1) {
|
||||||
className = 'first-item'
|
className = "first-item";
|
||||||
}
|
}
|
||||||
return className;
|
return className;
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
@ -103,12 +99,12 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.first-item {
|
&.first-item {
|
||||||
border-bottom-left-radius: .5rem;
|
border-bottom-left-radius: 0.5rem;
|
||||||
border-top-left-radius: .5rem;
|
border-top-left-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
&.last-item {
|
&.last-item {
|
||||||
border-bottom-right-radius: .5rem;
|
border-bottom-right-radius: 0.5rem;
|
||||||
border-top-right-radius: .5rem;
|
border-top-right-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ describe("radio.vue", () => {
|
||||||
loaderColor: "blue",
|
loaderColor: "blue",
|
||||||
loaderPosition: "right",
|
loaderPosition: "right",
|
||||||
sizeInRem: "1.5",
|
sizeInRem: "1.5",
|
||||||
errorMessage: 'null'
|
errorMessage: "null",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -20,14 +20,14 @@ describe("radio.vue", () => {
|
||||||
const label = wrapper.find("label");
|
const label = wrapper.find("label");
|
||||||
const paragraph = wrapper.find("span");
|
const paragraph = wrapper.find("span");
|
||||||
|
|
||||||
await label.trigger('click');
|
await label.trigger("click");
|
||||||
|
|
||||||
expect(input.attributes()).toEqual({
|
expect(input.attributes()).toEqual({
|
||||||
id: "2023",
|
id: "2023",
|
||||||
type: "radio",
|
type: "radio",
|
||||||
value: "2023",
|
value: "2023",
|
||||||
name: "TestGroup",
|
name: "TestGroup",
|
||||||
"aria-required": "true"
|
"aria-required": "false",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(label.attributes()).toEqual({
|
expect(label.attributes()).toEqual({
|
||||||
|
|
@ -35,14 +35,13 @@ describe("radio.vue", () => {
|
||||||
tabindex: "-1",
|
tabindex: "-1",
|
||||||
for: "2023",
|
for: "2023",
|
||||||
class: "d-flex flex-column justify-content-center py-3 px-4",
|
class: "d-flex flex-column justify-content-center py-3 px-4",
|
||||||
"aria-checked": "false"
|
"aria-checked": "false",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(label.text()).toEqual('2023');
|
expect(label.text()).toEqual("2023");
|
||||||
|
|
||||||
expect(paragraph.text()).toEqual('2023');
|
expect(paragraph.text()).toEqual("2023");
|
||||||
|
|
||||||
expect(wrapper.vm.display).toBe(true);
|
expect(wrapper.vm.display).toBe(true);
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<!-- See the component-test.vue page for example implementation -->
|
<!-- See the component-test.vue page for example implementation -->
|
||||||
<!-- role="radiogroup" and aria-labelledby must be included in the parent component for the radio group -->
|
|
||||||
<!-- Example: -->
|
|
||||||
<!-- <div role="radiogroup" aria-labelledby="demo-radio-group" class="col my-3 d-flex align-items-center flex-column"> -->
|
|
||||||
<!-- An h3 with id must be included just before the opening radio button group. ***The id must match the aria-labelledby of the parent div.*** -->
|
|
||||||
<!-- Example -->
|
|
||||||
<!-- <h3 class="visually-hidden" id="demo-radio-group">Select Vehicle Year</h3> -->
|
|
||||||
<div class="radiogroup radio-list-button d-flex flex-column w-100 mb-2">
|
<div class="radiogroup radio-list-button d-flex flex-column w-100 mb-2">
|
||||||
<input type="radio" :id="radioID" :name="groupName" :value="radioID" aria-required="true" @keyup.space="displayComponent">
|
<input type="radio" :id="radioID" :name="groupName" :value="radioID" :aria-required="isRequired" @keyup.space="displayComponent()">
|
||||||
<label role="radio" tabindex="-1" aria-checked="false" :for="radioID" class="d-flex flex-column justify-content-center py-3 px-4" @click='displayComponent'>
|
<label role="radio" tabindex="-1" aria-checked="false" :for="radioID" class="d-flex flex-column justify-content-center py-3 px-4" @click='displayComponent()'>
|
||||||
<span class="m-0" :class="[this.textPosition]">{{radioID}}</span>
|
<span class="m-0" :class="[this.textPosition]">{{radioID}}</span>
|
||||||
<span class="m-0 small" :class="[this.textPosition]">{{radioLabelSubCopy}}</span>
|
<span v-if="radioLabelSubCopy" class="m-0 small" :class="[this.textPosition]">{{radioLabelSubCopy}}</span>
|
||||||
|
<span v-if="screenReaderOnlyText" class="sr-only">{{screenReaderOnlyText}}</span>
|
||||||
<loader v-if="display" :style="{width: `${sizeInRem}rem`, height: `${sizeInRem}rem`}" :class="[this.loaderColor, this.loaderPosition]" />
|
<loader v-if="display" :style="{width: `${sizeInRem}rem`, height: `${sizeInRem}rem`}" :class="[this.loaderColor, this.loaderPosition]" />
|
||||||
</label>
|
</label>
|
||||||
<p class="small">{{errorMessage}}</p>
|
<p class="small">{{errorMessage}}</p>
|
||||||
|
|
@ -24,6 +19,8 @@ export default {
|
||||||
props: {
|
props: {
|
||||||
groupName: String, /* Required, unique for each radio button GROUP */
|
groupName: String, /* Required, unique for each radio button GROUP */
|
||||||
radioID: String, /* Required, unique for each radio button. Used for button id, label and <label for> */
|
radioID: String, /* Required, unique for each radio button. Used for button id, label and <label for> */
|
||||||
|
isRequired: Boolean, /* Optional, default is false */
|
||||||
|
screenReaderOnlyText: String, /* Optional, copy to be read by screenreader */
|
||||||
radioLabelSubCopy: String, /* Optional, used for multi-line radio buttons */
|
radioLabelSubCopy: String, /* Optional, used for multi-line radio buttons */
|
||||||
textPosition: String, /* Optional, use Bootstrap classes: text-start, text-center, text-end. Default (empty) is text-start */
|
textPosition: String, /* Optional, use Bootstrap classes: text-start, text-center, text-end. Default (empty) is text-start */
|
||||||
errorMessage: String, /* Optional, if there is an error message to be displayed */
|
errorMessage: String, /* Optional, if there is an error message to be displayed */
|
||||||
|
|
@ -34,7 +31,7 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isError: false,
|
isError: false,
|
||||||
display: false
|
display: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -44,6 +41,6 @@ export default {
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
loader,
|
loader,
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue