CSR-343 WIP

This commit is contained in:
Katie 2022-06-27 08:40:56 -04:00
parent f0748c4c0b
commit 9d7c6fc1fc
4 changed files with 313 additions and 275 deletions

View file

@ -1,44 +1,48 @@
<!-- Documented in confluence https://safelite.atlassian.net/wiki/spaces/DC/pages/76644418/Button+Question+Component -->
<template>
<div :class="isOverflowScrollable ? 'button-question button-question-overflow' : 'button-question'">
<div v-if="questionText" class="question-text d-flex">
<div v-if="questionText && answers && answers.length > 0" class="question-text d-flex">
<span class="fs-5 fw-bold w-100">{{ questionText }}</span>
</div>
<div class="w-100 d-flex justify-content-center">
<div class="w-100 justify-content-center">
<fieldset class="w-100" :aria-required=isRequired :class="getFieldSetClasses" :role="isMultiSelect ? 'group' : 'radiogroup'" :aria-labelledby="groupName">
<legend class="sr-only" :data-focus-target="groupName" :id="groupName" tabindex="-1">
{{(isMultiSelect && answers && answers.length > 1) ? 'Select one or more options below.' : 'Select an option below.' }}
</legend>
<div :class="getComponentWrapperClasses">
<component
:is="buttonType"
v-for="answer in answers"
:key="answer.Name ? answer.Name : answer"
@isCheckedChanged="handleCheckedChanged"
:buttonID="answer.Name ? groupName + '-' + answer.Name : groupName + '-' + answer"
:value="getValues(answer)"
:buttonLabel="answer.Text ? answer.Text : answer"
:buttonLabelSubCopy="answer.SubText"
:textPosition="textPosition"
:isMultiSelect="isMultiSelect"
:groupName="groupName"
:selectingInitiatesLoad="selectingInitiatesLoad"
:loaderColor="loaderColor"
:loaderPosition="loaderPosition"
:isWide=isWide
:isRequired=isRequired
:buttonImage="answer.AnswerImageUrl"
:buttonImageId="answer.ImageId"
:altText="answer.Name ? answer.Name : answer"
screenReaderOnlyText="(opens new window)"
:colLength="getColLength"
:selectedValues="selectedValues"
data-test="button"
:validationRules="validationRules"
:class="[suppressError ? 'alertError' : '']"
:clearOnUnmount="clearOnUnmount"
/>
</div>
<div :class="getComponentWrapperClasses" v-for="answer in answers" :key="answer.Name ? answer.Name : answer">
<component
:is="buttonType"
@isCheckedChanged="handleCheckedChanged"
:buttonID="answer.Name ? groupName + '-' + answer.Name : groupName + '-' + answer"
:value="getValues(answer)"
:buttonLabel="answer.Text ? answer.Text : answer"
:buttonLabelSubCopy="answer.SubText"
:textPosition="textPosition"
:isMultiSelect="isMultiSelect"
:groupName="groupName"
:selectingInitiatesLoad="selectingInitiatesLoad"
:loaderColor="loaderColor"
:loaderPosition="loaderPosition"
:isWide=isWide
:isRequired=isRequired
:buttonImage="answer.AnswerImageUrl"
:buttonImageId="answer.ImageId"
:altText="answer.Name ? answer.Name : answer"
screenReaderOnlyText="(opens new window)"
:colLength="getColLength"
:selectedValues="selectedValues"
data-test="button"
:validationRules="validationRules"
:class="[suppressError ? 'alertError' : '']"
:clearOnUnmount="clearOnUnmount"
/>
<!-- For nested questions -->
<transition name="fade" mode="out-in">
<div v-if="typeof selectedValues == 'string' && selectedValues == answer.Name">
<slot></slot>
</div>
</transition>
</div>
</fieldset>
</div>
<div class="row form-test-error mt-1">
@ -81,14 +85,14 @@ export default {
isRequired: Boolean,
isOverflowScrollable: Boolean,
isWide: Boolean,
modelValue: Array,
modelValue: [Array, String],
validationRules: String,
suppressError: Boolean,
useTextForValue: Boolean,
clearOnUnmount: {
type: Boolean,
default: true
}
},
},
computed: {
getFieldSetClasses() {
@ -135,6 +139,7 @@ export default {
if (this.useTextForValue){
return answer.Text
}
console.log(answer.Name ? answer.Name : answer)
return answer.Name ? answer.Name : answer;
},
handleCheckedChanged(val) {
@ -146,6 +151,9 @@ export default {
val.checkValue ? newSelectedValues.push(val.value) : newSelectedValues.splice(newSelectedValues.indexOf(val.value), 1);
this.selectedValues = newSelectedValues;
}
else {
this.selectedValues = val.value;
}
}
},
},

View file

@ -1,245 +1,262 @@
<template>
<div class="container">
<div class="row">
<p class="mb-0 color-question-text">{{ colorQuestionText }}</p>
<div class="container">
<div class="row">
<p class="mb-0 color-question-text">{{ colorQuestionText }}</p>
</div>
</div>
</div>
{{ featureListData }}
<br />
{{ tintSelectionOptions }}
<br />
{{ selectedTint }}
{{ selectedPart }}
<div
class="container nested-radio"
v-for="(value, name, index) in featureListData"
:key="index"
>
<div class="row my-2">
<div class="col">
<listCard
v-model="selectedTint[name]"
:value="`${glassLocation}-${glassName}-${name}`"
:isRadio="true"
:isWide="true"
:buttonImage="
require(`@/assets/img/tints/${getTintSourceImage(
glassLocation,
name
)}`)
"
:buttonLabel="name"
altText=""
isRequired
:buttonID="`${glassLocation}-${glassName}-${name}`"
:groupName="`${glassLocation}-${glassName}`"
@isCheckedChanged="ResetTintAndPartSelections()"
:validationRules="validationRules"
/>
<div class="row form-test-error mt-1">
<error-message :name="`${glassLocation}-${glassName}`" v-if="!suppressError"></error-message>
<div class="container nested-radio">
<div class="row my-2">
<div class="col">
<buttonQuestion
v-model="selectedTint"
:answers="tintSelectionOptions"
:nestedAnswers="featureListData[selectedTint]"
buttonType="listCard"
:isWide="true"
altText=""
isRequired
:groupName="`${glassLocation}-${glassName}`"
@isCheckedChanged="ResetTintAndPartSelections()"
:validationRules="validationRules"
>
<transition name="fade" mode="out-in">
<div class="row my-2" aria-live="polite">
<div class="col">
<buttonQuestion
v-model="selectedPart"
buttonType="radio"
class="radioQuestion"
:questionText="glassFeatureQuestion"
:answers="featureListData[selectedTint]"
textPosition="text-start"
:loaderEnabled="false"
isRequired
:groupName="`${glassLocation}-${glassName}-${selectedTint}`"
:validationRules="validationRules"
/>
</div>
</div>
</transition>
</buttonQuestion>
</div>
</div>
</div>
</div>
<transition name="fade" mode="out-in">
<div
v-if="
selectedTint[name] != undefined &&
selectedTint[name].buttonId ===
`${glassLocation}-${glassName}-${name}`
"
class="row my-2"
aria-live="polite"
>
<div class="col">
<buttonQuestion
v-model="selectedPart[glassLocation]"
buttonType="radio"
class="radioQuestion"
:questionText="glassFeatureQuestion"
:answers="value"
textPosition="text-start"
:loaderEnabled="false"
isRequired
:groupName="`${glassLocation}-${glassName}-${name}`"
:validationRules="validationRules"
/>
</div>
</div>
</transition>
</div>
<div class="row form-test-error mt-1">
<!-- <error-message
:name="`${glassLocation}-${glassName}`"
v-if="!suppressError"
></error-message> -->
</div>
</template>
<script>
// Components
import listCard from "@/ux-components/list-card/list-card";
import buttonQuestion from "@/common-components/button-question/button-question";
// Supporting files
import { getTintImage } from "@/constants/tint-mapper";
import { getCustomTransformValue } from "@/constants/dynamictext-mapper";
import { ErrorMessage } from 'vee-validate';
// import { ErrorMessage } from "vee-validate";
export default {
name: "glass-part-question",
inheritAttrs: false,
data() {
return {
glassColorQuestion: "",
glassFeatureQuestion: "",
selectedTint: {}, // v-model for the list card selections
};
},
props: {
glassName: String,
glassLocation: String,
colorAnswers: Array,
modelValue: Object,
validationRules: String,
},
mounted() {
this.LoadPreselectedValues();
},
components: {
listCard,
buttonQuestion,
ErrorMessage,
},
computed: {
colorQuestionText() {
return getCustomTransformValue(
this.glassColorQuestion,
`${this.glassLocation} ${this.glassName}`
);
name: "glass-part-question",
inheritAttrs: false,
data() {
return {
glassColorQuestion: "",
glassFeatureQuestion: "",
selectedTint: "",
};
},
selectedPart: {
get: function () {
return this.modelValue;
},
set: function (newValue) {
this.$emit("update:modelValue", newValue);
},
props: {
glassName: String,
glassLocation: String,
colorAnswers: Array,
modelValue: Object,
validationRules: String,
},
// Creates a map of the feature list data in the correct Name/Value
// format for the button-question component.
featureListData() {
const tintMapByColor = this.colorAnswers.reduce((arr, item) => {
// Create the key
const itemColor = item.ColorAnswerText;
arr[itemColor] = arr[itemColor] || [];
// Map the data to Name/Text object for ButtonQuestion
const mappedItem = item.FeatureAnswers.reduce((featureArr, item) => {
featureArr["Text"] = item.FeatureAnswerText; // Display to User
featureArr["Name"] = item.PartNumber; // Backing Value
return featureArr;
}, {});
// Add onto the final object
arr[itemColor].push(mappedItem);
return arr;
}, {});
return tintMapByColor;
mounted() {
this.LoadPreselectedValues();
},
PartDataFromApi() {
return this.$store.getters.pageData(this.$route.query.fmgPage);
},
},
methods: {
// Initialize the component data
initializeComponent(cmsContent) {
this.glassColorQuestion = cmsContent.ColorQuestionWidget.QuestionText;
this.glassFeatureQuestion = cmsContent.FeatureQuestionWidget.QuestionText;
components: {
buttonQuestion,
// ErrorMessage,
},
computed: {
colorQuestionText() {
return getCustomTransformValue(
this.glassColorQuestion,
`${this.glassLocation} ${this.glassName}`
);
},
// Gets tint images based on the glass type, and tint name.
// Returns an empty string if the src or object is undefined.
getTintSourceImage(glassLocation, tintColor) {
const tintSourceObject = getTintImage(glassLocation, tintColor);
if (tintSourceObject === undefined || tintSourceObject.src == undefined) {
return "";
}
return tintSourceObject.src;
},
// Reset selections when tint changes for the same glass to ensure proper selection.
// Also checks if only a single part is present for the tint.
ResetTintAndPartSelections() {
this.selectedTint = {};
this.selectedPart = {};
this.AutoSelectIfSinglePart();
},
// Check if only a single part is present for the tint and set the v-model if it is.
AutoSelectIfSinglePart() {
// Check if the selected tint only has a single feature
Object.keys(this.PartDataFromApi.partsOrQuestions ?? {}).forEach(
(key) => {
const currentGlassSelection =
this.PartDataFromApi.partsOrQuestions[key];
if (
currentGlassSelection.glassName === this.glassName &&
currentGlassSelection.glassLocation === this.glassLocation
) {
if (currentGlassSelection.parts.length === 1) {
this.selectedPart = {
[currentGlassSelection.glassLocation]: [
currentGlassSelection.parts[0].partNumber,
],
};
}
}
}
);
},
// Loads the preselected values from the store.
LoadPreselectedValues() {
this.$nextTick(() => {
if (this.modelValue !== undefined) {
// Populate button-question model-value if parts data already exists in VueX
const alreadyPopulatedPartsData =
this.$store.getters.lineItems.glassParts;
Object.keys(alreadyPopulatedPartsData).forEach((key) => {
const partNumber = alreadyPopulatedPartsData[key].partNumber;
const tintColor = alreadyPopulatedPartsData[key].color;
Object.keys(this.modelValue).forEach((key) => {
if (this.modelValue[key][0] === partNumber) {
this.selectedTint[tintColor] = {
buttonId: `${this.glassLocation}-${this.glassName}-${tintColor}`,
checkValue: "",
value: `${this.glassLocation}-${this.glassName}-${tintColor}`,
};
}
tintSelectionOptions() {
let tintOptions = [];
Object.keys(this.featureListData).forEach((tintOption) => {
tintOptions.push({
Name: tintOption,
Text: tintOption,
AnswerImageUrl: require(`@/assets/img/tints/${this.getTintSourceImage(
this.glassLocation,
tintOption
)}`),
});
});
});
}
});
return tintOptions;
},
selectedPart: {
get: function () {
return this.modelValue;
},
set: function (newValue) {
console.log("modelValue: " + newValue)
this.$emit("update:modelValue", newValue);
},
},
// Creates a map of the feature list data in the correct Name/Value
// format for the button-question component.
featureListData() {
const tintMapByColor = this.colorAnswers.reduce((arr, item) => {
// Create the key
const itemColor = item.ColorAnswerText;
arr[itemColor] = arr[itemColor] || [];
// Map the data to Name/Text object for ButtonQuestion
const mappedItem = item.FeatureAnswers.reduce(
(featureArr, item) => {
featureArr["Text"] = item.FeatureAnswerText; // Display to User
featureArr["Name"] = item.PartNumber; // Backing Value
return featureArr;
},
{}
);
// Add onto the final object
arr[itemColor].push(mappedItem);
return arr;
}, {});
return tintMapByColor;
},
PartDataFromApi() {
return this.$store.getters.pageData(this.$route.query.fmgPage);
},
},
methods: {
// Initialize the component data
initializeComponent(cmsContent) {
this.glassColorQuestion =
cmsContent.ColorQuestionWidget.QuestionText;
this.glassFeatureQuestion =
cmsContent.FeatureQuestionWidget.QuestionText;
},
// Gets tint images based on the glass type, and tint name.
// Returns an empty string if the src or object is undefined.
getTintSourceImage(glassLocation, tintColor) {
const tintSourceObject = getTintImage(glassLocation, tintColor);
if (
tintSourceObject === undefined ||
tintSourceObject.src == undefined
) {
return "";
}
return tintSourceObject.src;
},
// Reset selections when tint changes for the same glass to ensure proper selection.
// Also checks if only a single part is present for the tint.
ResetTintAndPartSelections() {
this.selectedTint = "";
this.selectedPart = {};
this.AutoSelectIfSinglePart();
},
// Check if only a single part is present for the tint and set the v-model if it is.
AutoSelectIfSinglePart() {
// Check if the selected tint only has a single feature
Object.keys(this.PartDataFromApi.partsOrQuestions ?? {}).forEach(
(key) => {
const currentGlassSelection =
this.PartDataFromApi.partsOrQuestions[key];
if (
currentGlassSelection.glassName === this.glassName &&
currentGlassSelection.glassLocation ===
this.glassLocation
) {
if (currentGlassSelection.parts.length === 1) {
this.selectedPart = {
[currentGlassSelection.glassLocation]: [
currentGlassSelection.parts[0].partNumber,
],
};
}
}
}
);
},
// Loads the preselected values from the store.
LoadPreselectedValues() {
this.$nextTick(() => {
if (this.modelValue !== undefined) {
// Populate button-question model-value if parts data already exists in VueX
const alreadyPopulatedPartsData =
this.$store.getters.lineItems.glassParts;
Object.keys(alreadyPopulatedPartsData).forEach((key) => {
const partNumber =
alreadyPopulatedPartsData[key].partNumber;
const tintColor = alreadyPopulatedPartsData[key].color;
Object.keys(this.modelValue).forEach((key) => {
if (this.modelValue[key][0] === partNumber) {
this.selectedTint = tintColor;
// this.selectedTint[tintColor] = {
// buttonId: `${this.glassLocation}-${this.glassName}-${tintColor}`,
// checkValue: "",
// value: `${this.glassLocation}-${this.glassName}-${tintColor}`,
// };
}
});
});
}
});
},
},
},
};
</script>
<style lang="scss">
.nested-radio {
.ui-radio {
flex-direction: column;
margin: 0.25rem 0;
}
.ui-radio {
flex-direction: column;
margin: 0.25rem 0;
}
p {
font-size: 0.875rem;
}
p {
font-size: 0.875rem;
}
}
.color-question-text {
color: $black;
font-weight: $font-weight-bold;
color: $black;
font-weight: $font-weight-bold;
}
</style>

View file

@ -30,13 +30,13 @@
</div>
<glassPartQuestion
:ref="`${RefPrefix}-${item.glassLocation}-${item.glassName}`"
v-model="glassParts[item.glassLocation + '-' + item.glassName]"
:glassLocation="item.glassLocation"
:glassName="item.glassName"
:colorAnswers="item.colorAnswers"
validationRules="replace-options-required"
/>
:ref="`${RefPrefix}-${item.glassLocation}-${item.glassName}`"
v-model="glassParts[item.glassLocation + '-' + item.glassName]"
:glassLocation="item.glassLocation"
:glassName="item.glassName"
:colorAnswers="item.colorAnswers"
validationRules="replace-options-required"
/>
</div>
<funnelFooter
cmsWidgetName="FunnelFooterWidget"
@ -170,41 +170,54 @@ methods: {
// Compile all selected parts from the page.
for (let [key, value] of Object.entries(this.glassParts)) {
for (let [glassKey, glassValue] of Object.entries(value)) {
selectedGlassPartNumbers.push(glassValue[0]);
}
selectedGlassPartNumbers.push(value);
// for (let [glassKey, glassValue] of Object.entries(value)) {
// selectedGlassPartNumbers.push(glassValue[0]);
// }
}
console.log(this.glassParts)
console.log(selectedGlassPartNumbers)
console.log(this.PartsFromApi.partsOrQuestions)
// Match them to the parts from the API.
for (let [key, value] of Object.entries(
this.PartsFromApi.partsOrQuestions
)) {
for (let [partKey, partValue] of Object.entries(value.parts)) {
const currentPart =
this.PartsFromApi.partsOrQuestions[key].parts[partKey];
const isMatched = selectedGlassPartNumbers.some(
(p) => p === currentPart.partNumber
);
for (let [key, value] of Object.entries(this.PartsFromApi.partsOrQuestions)) {
// const partsForGlassPart = value
const isMatched = selectedGlassPartNumbers.some(
(p) => p === value.partNumber
);
if (isMatched) {
matchedParts.push(currentPart);
}
if (isMatched) {
matchedParts.push(currentPart);
}
// for (let [partKey, partValue] of Object.entries(value.parts)) {
// const currentPart = this.PartsFromApi.partsOrQuestions[key].parts[partKey];
// const isMatched = selectedGlassPartNumbers.some(
// (p) => p === currentPart.partNumber
// );
// if (isMatched) {
// matchedParts.push(currentPart);
// }
// }
}
console.log(matchedParts)
// If no parts could be matched, throw an error.
if (matchedParts.length === 0) {
throw new Error("Could not match any parts to the selected parts");
}
// Save parts to the store.
store.commit(storeMutations.UPDATE_GLASS_PARTS, matchedParts);
// // Save parts to the store.
// store.commit(storeMutations.UPDATE_GLASS_PARTS, matchedParts);
// Navigate to the next page.
this.$router.navigateAfterSave(
this.navigationScenarios.SELECTED_PARTS,
this.$route
);
// // Navigate to the next page.
// this.$router.navigateAfterSave(
// this.navigationScenarios.SELECTED_PARTS,
// this.$route
// );
},
resetDependentState() {

View file

@ -86,7 +86,7 @@ export default {
};
</script>
<style lang="scss" scoped>
<style lang="scss" scoped>yu78zas
.form-check {
.form-check-input {
border: 1px solid $gray-500;