Fixed merge conflicts

This commit is contained in:
brydon1 2023-07-12 14:24:50 -04:00
commit 9eb36449a7
24 changed files with 2543 additions and 497 deletions

View file

@ -1,21 +1,45 @@
module.exports = {
extends: [
'eslint-config-airbnb-base',
'plugin:vue/vue3-strongly-recommended'
],
rules: {
'linebreak-style': 'off',
'vue/component-definition-name-casing': ['warn', 'kebab-case'],
'vue/require-default-prop': 'off',
'vue/attribute-hyphenation': ['warn', 'never'],
'vue/v-on-event-hyphenation': ['warn', 'never']
},
settings: {
'import/resolver': {
alias: {
map: [['@', './src/']],
extensions: ['.js', '.vue']
}
module.exports = {
env: {
browser: true,
jest: true
},
extends: [
'eslint-config-airbnb-base',
'plugin:vue/vue3-recommended'
],
rules: {
'linebreak-style': 'off',
'vue/component-definition-name-casing': ['warn', 'kebab-case'],
'vue/require-default-prop': 'off',
'vue/attribute-hyphenation': ['warn', 'never'],
'vue/v-on-event-hyphenation': ['warn', 'never'],
'object-curly-newline': ['error', { consistent: true }],
'function-paren-newline': ['error', 'never'],
'operator-linebreak': ['error', 'before'],
'implicit-arrow-linebreak': ['error', 'below'],
'comma-dangle': ['error', 'never'],
indent: ['error', 4],
'vue/html-indent': 'off',
'vue/html-closing-bracket-newline': ['error', {
singleline: 'never',
multiline: 'never'
}],
'vue/html-self-closing': ['error', {
html: {
void: 'any',
normal: 'any',
component: 'any'
},
svg: 'always',
math: 'always'
}]
},
settings: {
'import/resolver': {
alias: {
map: [['@', './src/']],
extensions: ['.js', '.vue']
}
}
}
}
};
};

1036
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -40,6 +40,7 @@
"eslint-plugin-vue": "9.8.0",
"jest": "^27.0.5",
"sass": "^1.32.7",
"sass-loader": "^12.0.0"
"sass-loader": "^12.0.0",
"vitest": "^0.32.4"
}
}

View file

@ -0,0 +1,22 @@
/**
* @file global-rules.js
* @author MB
* @copyright Safelite
*/
/**
* @summary Contains all the globally defined rules.
*/
const globalRules = {
POLICYHOLDER_FIRST_NAME_REQUIRED: 'policyholder-first-name-required',
POLICYHOLDER_LAST_NAME_REQUIRED: 'policyholder-last-name-required',
FIRST_NAME_REQUIRED: 'first-name-required',
LAST_NAME_REQUIRED: 'last-name-required',
PHONE_NUMBER_REQUIRED: 'phone-number-required',
EMAIL_ADDRESS_REQUIRED: 'email-required',
EMAIL_ADDRESS_FORMAT: 'email-address-format',
PHONE_NUMBER_FORMAT: 'phone-number-format',
OPTION_REQUIRED: 'option-required'
};
export default globalRules;

View file

@ -0,0 +1,348 @@
// Components
import textareaQuestion from '@/digital-components/textarea-question/textarea-question.vue';
// Supporting Files
import { shallowMount } from '@vue/test-utils';
import { getRandomString, getRandomInt } from '@/helpers/data-generation';
import { nextTick } from 'vue';
import { defineRule } from 'vee-validate';
import { required } from '@/helpers/validation-rules';
describe('textarea-question.vue', () => {
const mockMixin = {
methods: {
getCmsContent: jest.fn().mockImplementation(() =>
getRandomString(10, 20)),
setCmsContent: jest.fn()
}
};
describe('Expected components rendered', () => {
test('Label rendered', () => {
// Arrange
const questionText = getRandomString(10, 20);
const mixinWithQuestion = {
methods: {
getCmsContent: jest.fn().mockImplementation(() =>
questionText),
setCmsContent: jest.fn()
}
};
const wrapper = shallowMount(textareaQuestion, {
propsData: {
inputId: getRandomString(10, 20),
cmsWidgetName: getRandomString(10, 20)
},
mixins: [mixinWithQuestion]
});
// Act
const label = wrapper.find('label');
// Assert
expect(label.exists()).toBe(true);
expect(wrapper.text()).toContain(questionText);
});
test('Textarea rendered', () => {
// Arrange
const wrapper = shallowMount(textareaQuestion, {
propsData: {
inputId: getRandomString(10, 20),
cmsWidgetName: getRandomString(10, 20)
},
mixins: [mockMixin]
});
// Act
const textarea = wrapper.find('textarea');
// Assert
expect(textarea.exists()).toBe(true);
});
test('Character count rendered', () => {
// Arrange
const maxLength = getRandomInt(100, 900);
const wrapper = shallowMount(textareaQuestion, {
propsData: {
inputId: getRandomString(10, 20),
cmsWidgetName: getRandomString(10, 20),
maxLength
},
mixins: [mockMixin]
});
const expected = `0/${maxLength}`;
// Act
// Assert
expect(wrapper.text()).toContain(expected);
});
test('Error rendered', async () => {
// Arrange
const maxLength = getRandomInt(100, 900);
const wrapper = shallowMount(textareaQuestion, {
propsData: {
inputId: getRandomString(10, 20),
cmsWidgetName: getRandomString(10, 20),
maxLength
},
mixins: [mockMixin]
});
// Act
const errorComponent = wrapper.find('#error-message');
expect(errorComponent.text()).toBe('');
const errorMessage = getRandomString(20, 30);
wrapper.vm.resetField({
errors: [errorMessage]
});
await nextTick();
// Assert
expect(errorComponent.text()).toBe(errorMessage);
});
test('Not required => optional text rendered', () => {
// Arrange
const wrapper = shallowMount(textareaQuestion, {
propsData: {
inputId: getRandomString(10, 20),
cmsWidgetName: getRandomString(10, 20),
isRequired: false
},
mixins: [mockMixin]
});
// Act
// Assert
expect(wrapper.text()).toContain('Optional');
});
test('Required => optional text not rendered', () => {
// Arrange
const wrapper = shallowMount(textareaQuestion, {
propsData: {
inputId: getRandomString(10, 20),
cmsWidgetName: getRandomString(10, 20),
isRequired: true
},
mixins: [mockMixin]
});
// Act
// Assert
expect(wrapper.text()).not.toContain('Optional');
});
});
describe('Computed properties rendered', () => {
describe('characterCount', () => {
test('Model value null => character count is 0 ', () => {
// Arrange
const wrapper = shallowMount(textareaQuestion, {
propsData: {
inputId: getRandomString(10, 20),
cmsWidgetName: getRandomString(10, 20),
modelValue: null
},
mixins: [mockMixin]
});
// Act
// Assert
expect(wrapper.vm.characterCount).toBe(0);
});
test('Model value undefined => character count is 0', () => {
// Arrange
const wrapper = shallowMount(textareaQuestion, {
propsData: {
inputId: getRandomString(10, 20),
cmsWidgetName: getRandomString(10, 20),
modelValue: undefined
},
mixins: [mockMixin]
});
// Act
// Assert
expect(wrapper.vm.characterCount).toBe(0);
});
test('Model value empty string => character count is 0', () => {
// Arrange
const wrapper = shallowMount(textareaQuestion, {
propsData: {
inputId: getRandomString(10, 20),
cmsWidgetName: getRandomString(10, 20),
modelValue: ''
},
mixins: [mockMixin]
});
// Act
// Assert
expect(wrapper.vm.characterCount).toBe(0);
});
test('Model value non-empty string => character count is as expected', () => {
// Arrange
const length = getRandomInt(10, 30);
const value = getRandomString(length, length);
const wrapper = shallowMount(textareaQuestion, {
propsData: {
inputId: getRandomString(10, 20),
cmsWidgetName: getRandomString(10, 20),
modelValue: value
},
mixins: [mockMixin]
});
// Act
// Assert
expect(wrapper.vm.characterCount).toBe(length);
});
});
describe('value', () => {
test('Get value corresponds to modelValue', () => {
// Arrange
const modelValue = getRandomString(10, 20);
const wrapper = shallowMount(textareaQuestion, {
propsData: {
inputId: getRandomString(10, 20),
cmsWidgetName: getRandomString(10, 20),
modelValue
},
mixins: [mockMixin]
});
// Act
// Assert
expect(wrapper.vm.value).toBe(modelValue);
});
test('Set value => update modelValue event emitted with new value', () => {
// Arrange
const modelValue = getRandomString(10, 20);
const wrapper = shallowMount(textareaQuestion, {
propsData: {
inputId: getRandomString(10, 20),
cmsWidgetName: getRandomString(10, 20),
modelValue
},
mixins: [mockMixin]
});
const newValue = getRandomString(10, 20);
// Act
wrapper.vm.value = newValue;
// Assert
expect(wrapper.emitted('update:modelValue')).toBeTruthy();
expect(wrapper.emitted('update:modelValue')[0][0]).toEqual(newValue);
});
});
});
describe('Properties properly affect initial state', () => {
test('Placeholder text sets placeholder text of text area', () => {
// Arrange
const placeholderText = getRandomString(10, 20);
const wrapper = shallowMount(textareaQuestion, {
propsData: {
inputId: getRandomString(10, 20),
cmsWidgetName: getRandomString(10, 20),
placeholderText
},
mixins: [mockMixin]
});
// Act
const textarea = wrapper.find('textarea');
// Assert
expect(textarea.attributes().placeholder).toBe(placeholderText);
});
test('isDisabled true => textarea disabled', () => {
// Arrange
const wrapper = shallowMount(textareaQuestion, {
propsData: {
inputId: getRandomString(10, 20),
cmsWidgetName: getRandomString(10, 20),
isDisabled: true
},
mixins: [mockMixin]
});
// Act
const textarea = wrapper.find('textarea');
// Assert
expect(textarea.attributes().disabled).toBeDefined();
});
test('isRequired true => textarea required', () => {
// Arrange
const wrapper = shallowMount(textareaQuestion, {
propsData: {
inputId: getRandomString(10, 20),
cmsWidgetName: getRandomString(10, 20),
isRequired: true
},
mixins: [mockMixin]
});
// Act
const textarea = wrapper.find('textarea');
// Assert
expect(textarea.attributes().required).toBeDefined();
});
test('validationRules set rules of text area', () => {
// Arrange
const rule = getRandomString(10, 20);
const errorMessage = getRandomString(20, 30);
defineRule(rule, required(errorMessage));
const wrapper = shallowMount(textareaQuestion, {
propsData: {
inputId: getRandomString(10, 20),
cmsWidgetName: getRandomString(10, 20),
validationRules: rule
},
mixins: [mockMixin]
});
// Act
const textarea = wrapper.find('textarea');
// Assert
expect(textarea.attributes().validationrules).toBe(rule);
});
test('maxLength set max length of text area', () => {
// Arrange
const maxLength = toString(getRandomInt(100, 900));
const wrapper = shallowMount(textareaQuestion, {
propsData: {
inputId: getRandomString(10, 20),
cmsWidgetName: getRandomString(10, 20),
maxLength
},
mixins: [mockMixin]
});
// Act
const textarea = wrapper.find('textarea');
// Assert
expect(textarea.attributes().maxlength).toBe(maxLength);
});
test('inputRows set input rows of text area', () => {
// Arrange
const inputRows = getRandomInt(10, 20);
const wrapper = shallowMount(textareaQuestion, {
propsData: {
inputId: getRandomString(10, 20),
cmsWidgetName: getRandomString(10, 20),
inputRows
},
mixins: [mockMixin]
});
// Act
const textarea = wrapper.find('textarea');
// Assert
expect(textarea.attributes().rows).toBe(`${inputRows}`);
});
});
});

View file

@ -0,0 +1,197 @@
<template>
<div
class="textarea-question"
:class="(errors && errors.length) ? 'has-error' : ''">
<label
:for="inputId"
class="form-label">
{{ questionText }}
<span
v-if="!isRequired"
class="optional">(Optional)</span>
</label>
<div class="input-wrapper">
<textarea
:id="inputId"
:ref="inputId"
v-model.trim="value"
class="form-control"
:name="inputId"
:rows="[inputRows ?? 10]"
:placeholder="placeholderText"
:disabled="isDisabled"
:required="isRequired"
:maxlength="maxLength"
:validationRules="validationRules"
@change="validationRules ? handleChange : () => {}"
@blur="validationRules ? handleBlur : () => {}"
@paste="trimOnPaste"
@drop="trimOnPaste">
</textarea>
</div>
<p class="character-count margin-top-8">
{{ characterCount }}/{{ maxLength }} characters remaining
</p>
<div
v-show="errorMessage"
id="error-message"
class="row my-1 form-test-error">
<span
class="d-inline-flex mt-0"
role="alert">{{ errorMessage }}
</span>
</div>
</div>
</template>
<script>
import { useField } from 'vee-validate';
export default {
name: 'textarea-question',
props: {
placeholderText: {
type: String,
default: ''
},
inputId: {
type: String,
required: true
},
modelValue: String,
isDisabled: Boolean,
isRequired: Boolean,
validationRules: String,
cmsWidgetName: {
type: String,
required: true
},
maxLength: {
type: String,
default: '300'
},
inputRows: Number
},
emits: ['update:modelValue'],
setup(props) {
const propsClone = { ...props };
const { modelValue } = propsClone;
const initialValue = modelValue ?? '';
const fieldOptions = {
type: 'text',
value: modelValue,
initialValue
};
const { errorMessage,
handleChange,
handleBlur,
validate,
errors,
resetField }
= useField(props.inputId,
props.validationRules,
fieldOptions);
return {
errorMessage,
handleChange,
handleBlur,
validate,
errors,
resetField
};
},
computed: {
/**
* @summary Returns the number of characters in the textarea field.
*/
characterCount() {
return this?.modelValue?.length ?? 0;
},
/**
* @summary Returns the CMS text associated with the question.
*/
questionText() {
return this.getCmsContent(this.cmsWidgetName, 'QuestionText');
},
value: {
get() {
return this.modelValue;
},
set(newValue) {
this.$emit('update:modelValue', newValue);
}
}
},
methods: {
/**
* @summary Removes whitespace from the end of pasted content.
*/
trimOnPaste(evt) {
evt.stopPropagation();
evt.preventDefault();
const data = evt.type === 'paste' ? (evt.clipboardData || window.clipboardData) : evt.dataTransfer;
const value = data.getData('Text')?.trim();
this.$emit('update:modelValue', value);
}
}
};
</script>
<style lang="scss">
.textarea-question {
label {
color: $black;
font-weight: 500;
}
.form-test-error span{
color: $red;
font-size: 0.875rem;
font-weight: 500;
}
.form-control {
max-height: 30rem;
border: 1px solid $gray-500;
border-radius: 0.5rem;
padding: 12px 16px;
&::placeholder {
color: $gray-500;
}
&:focus {
box-shadow: 0 0 0 2.5px $blue;
}
&:disabled,
&.disabled {
background-color: $gray-100;
&:hover {
box-shadow: 0 0 0 4px transparent;
border: 1px solid $gray-500;
}
}
&:hover {
border: 1px solid $gray-500;
box-shadow: 0 0 0 4px $blue-300;
}
}
}
.optional {
color: $gray-550;
}
.character-count {
color: $gray-600;
font-size: 12px;
margin-bottom: 0px;
}
.margin-top-8 {
margin-top: 8px;
}
</style>

View file

@ -1,325 +1,340 @@
<template>
<div class="textbox-question" :class="(errors && errors.length) || hasError ? 'has-error' : ''">
<label
v-if="displayQuestionText"
:for="inputId"
:aria-label="questionText"
class="form-label"
:class="[questionAlignment === 'center' ? 'text-center w-100 mb-5' : '']"
v-html="labelText"></label>
<div
class="input-wrapper"
:class="[
includeSearchIcon ? 'has-search-icon' : '',
includeSelectIcon ? 'has-select-icon' : '',
]">
<input
class="form-control"
v-model.trim.lazy="value"
v-maska="mask"
:type="type"
:ref="inputId"
:id="inputId"
:name="inputId"
:placeholder="placeholderText"
:aria-disabled="isDisabled"
:disabled="isDisabled"
:aria-required="isRequired"
:aria-label="questionText"
:min="min"
:max="max"
required
:class="[
hasIcon ? 'has-icon' : '',
iconRight ? 'icon-right' : '',
cornerStyle === 'rounded' ? 'rounded-pill' : '',
]"
:validationRules="validationRules"
@change="validationRules ? handleChange : () => {}"
@blur="validationRules ? handleChange : () => {}"
:maxlength="maxLength ? maxLength : '999'"
@focus="$emit('focus', $event.target.value)"
:data-bs-toggle="includeSelectIcon ? 'modal' : ''"
:data-bs-target="'#' + this.cmsWidgetName"
@paste="trimOnPaste"
@drop="trimOnPaste"/>
<button v-if="includeSearchIcon" type="submit" aria-label="Search button" />
<button v-if="includeSelectIcon" type="submit" data-bs-toggle="modal"
:data-bs-target="'#' + this.cmsWidgetName" aria-label="Select button" />
</div>
<div v-show="errorMessage" class="row my-1 form-test-error">
<span class="d-inline-flex mt-0" role="alert">{{ errorMessage }}</span>
</div>
<div
class="textbox-question"
:class="(errors && errors.length) || hasError ? 'has-error' : ''"
>
<label
v-if="displayQuestionText"
:for="inputId"
:aria-label="questionText"
class="form-label"
:class="[questionAlignment === 'center' ? 'text-center w-100 mb-5' : '']"
v-html="labelText"
></label>
<div
class="input-wrapper"
:class="[
includeSearchIcon ? 'has-search-icon' : '',
includeSelectIcon ? 'has-select-icon' : '',
]"
>
<input
class="form-control"
v-model.trim.lazy="value"
v-maska="mask"
:type="type"
:ref="inputId"
:id="inputId"
:name="inputId"
:placeholder="placeholderText"
:aria-disabled="isDisabled"
:disabled="isDisabled"
:aria-required="isRequired"
:aria-label="questionText"
:min="min"
:max="max"
required
:class="[
hasIcon ? 'has-icon' : '',
iconRight ? 'icon-right' : '',
cornerStyle === 'rounded' ? 'rounded-pill' : '',
]"
:validationRules="validationRules"
@change="validationRules ? handleChange : () => {}"
@blur="validationRules ? handleChange : () => {}"
:maxlength="maxLength ? maxLength : '999'"
@focus="$emit('focus', $event.target.value)"
:data-bs-toggle="includeSelectIcon ? 'modal' : ''"
:data-bs-target="'#' + this.cmsWidgetName"
@paste="trimOnPaste"
@drop="trimOnPaste"
/>
<button
v-if="includeSearchIcon"
type="submit"
aria-label="Search button"
/>
<button
v-if="includeSelectIcon"
type="submit"
data-bs-toggle="modal"
:data-bs-target="'#' + this.cmsWidgetName"
aria-label="Select button"
/>
</div>
<div v-show="errorMessage" class="row my-1 form-test-error">
<span class="d-inline-flex mt-0" role="alert">{{ errorMessage }}</span>
</div>
</div>
</template>
<script>
import { useField, validate } from "vee-validate";
export default {
name: "textbox-question",
props: {
type: {
type: String,
default: "text",
},
placeholderText: {
type: String,
default: "",
},
displayQuestionText: {
type: Boolean,
default: true,
},
modelValue: String,
inputId: {
type: String,
required: true,
},
isDisabled: Boolean,
isRequired: Boolean,
hasIcon: Boolean, // If input has an icon
iconRight: Boolean, // Place icon on right side of text input, otherwise default is left if hasIcon prop is used
hasError: Boolean,
mask: {
type: String,
default: "",
},
validationRules: String,
cmsWidgetName: String,
maxLength: String,
questionAlignment: String, // Left or center. Left is default.
cornerStyle: String, // Rounded or square. Square is default.
includeSearchIcon: Boolean,
includeSelectIcon: Boolean,
min: String,
max: String,
disableAutoFill: Boolean
name: "textbox-question",
props: {
type: {
type: String,
default: "text",
},
setup(props) {
const propsClone = Object.assign({}, props);
const modelValue = propsClone.modelValue;
let initialValue;
placeholderText: {
type: String,
default: "",
},
displayQuestionText: {
type: Boolean,
default: true,
},
modelValue: String,
inputId: {
type: String,
required: true,
},
isDisabled: Boolean,
isRequired: Boolean,
hasIcon: Boolean, // If input has an icon
iconRight: Boolean, // Place icon on right side of text input, otherwise default is left if hasIcon prop is used
hasError: Boolean,
mask: {
type: String,
default: "",
},
validationRules: String,
cmsWidgetName: String,
maxLength: String,
questionAlignment: String, // Left or center. Left is default.
cornerStyle: String, // Rounded or square. Square is default.
includeSearchIcon: Boolean,
includeSelectIcon: Boolean,
min: String,
max: String,
disableAutoFill: Boolean,
},
setup(props) {
const propsClone = Object.assign({}, props);
const modelValue = propsClone.modelValue;
let initialValue;
switch (typeof modelValue) {
case "number":
initialValue = modelValue;
break;
default:
initialValue = modelValue && modelValue.length > 0 ? modelValue : "";
break;
switch (typeof modelValue) {
case "number":
initialValue = modelValue;
break;
default:
initialValue = modelValue && modelValue.length > 0 ? modelValue : "";
break;
}
const fieldOptions = {
type: "text",
value: modelValue,
initialValue: initialValue,
};
const { errorMessage, handleBlur, handleChange, meta, validate, errors } =
useField(props.inputId, props.validationRules, fieldOptions);
return {
errorMessage,
handleBlur,
handleChange,
validate,
meta,
errors,
};
},
methods: {
trimOnPaste(evt) {
evt.stopPropagation();
preventDefault();
var data = null;
if (evt.type === "paste") {
data = evt.clipboardData || window.clipboardData;
} else if (evt.type === "drop") {
data = evt.dataTransfer;
}
const value = data.getData("Text")?.trim();
this.$emit("update:modelValue", value);
},
},
computed: {
questionText() {
return this.getCmsContent(this.cmsWidgetName, "QuestionText");
},
value: {
get: function () {
return this.modelValue;
},
set: function (newValue) {
this.$emit("update:modelValue", newValue);
},
},
labelText: {
get: function () {
const noBreakChar = "&NoBreak;";
var questionText = "";
if (this.disableAutoFill) {
var words = this.questionText.toString().split(/[ ]+/);
words.forEach(function (word) {
const position = 1;
word = [
word.toString().slice(0, position),
noBreakChar,
word.toString().slice(position),
].join("");
questionText += `${word} `;
});
questionText = questionText.trimEnd();
} else {
questionText = this.questionText.toString();
}
const fieldOptions = {
type: "text",
value: modelValue,
initialValue: initialValue,
};
const { errorMessage, handleBlur, handleChange, meta, validate, errors } = useField(
props.inputId,
props.validationRules,
fieldOptions
);
return {
errorMessage,
handleBlur,
handleChange,
validate,
meta,
errors,
};
return questionText;
},
},
methods: {
trimOnPaste(evt) {
evt.stopPropagation();
evt.preventDefault();
const data = evt.type == "paste" ? (evt.clipboardData || window.clipboardData) : evt.dataTransfer;
const value = data.getData('Text')?.trim();
this.$emit("update:modelValue", value);
},
},
computed: {
questionText() {
return this.getCmsContent(this.cmsWidgetName, "QuestionText");
},
value: {
get: function () {
return this.modelValue;
},
set: function (newValue) {
this.$emit("update:modelValue", newValue);
},
},
labelText: {
get: function () {
const noBreakChar = "&NoBreak;";
var questionText = "";
if (this.disableAutoFill) {
var words = this.questionText.toString().split(/[ ]+/);
words.forEach(function (word) {
const position = 1;
word = [
word.toString().slice(0, position),
noBreakChar,
word.toString().slice(position),
].join("");
questionText += `${word} `;
});
questionText = questionText.trimEnd();
} else {
questionText = this.questionText.toString();
}
return questionText;
},
},
},
mounted() {
this.$emit("textboxQuestionEvent.inputIdAssigned", this.inputId);
},
watch: {
async value(newValue) {
const result = await validate(newValue, this.validationRules); // do a test validation check, without triggering full validation
if (result.valid) {
this.handleChange(newValue); // trigger full validation on this field only
}
},
},
mounted() {
this.$emit("textboxQuestionEvent.inputIdAssigned", this.inputId);
},
watch: {
async value(newValue) {
const result = await validate(newValue, this.validationRules); // do a test validation check, without triggering full validation
if (result.valid) {
this.handleChange(newValue); // trigger full validation on this field only
}
},
},
};
</script>
<style lang="scss">
input[type="date"]::-webkit-inner-spin-button{
display: none;
input[type="date"]::-webkit-inner-spin-button {
display: none;
}
input[type="date"]::-webkit-calendar-picker-indicator {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 1px;
background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M14.1762 1.52764H13.7656V1.50352C13.7656 1.10476 13.6076 0.722334 13.3263 0.44037C13.0451 0.158406 12.6636 0 12.2659 0C11.8681 0 11.4866 0.158406 11.2054 0.44037C10.9241 0.722334 10.7661 1.10476 10.7661 1.50352V1.52764H5.42476V1.50352C5.42476 1.10476 5.26675 0.722334 4.9855 0.44037C4.70424 0.158406 4.32277 0 3.92501 0C3.52725 0 3.14579 0.158406 2.86453 0.44037C2.58327 0.722334 2.42526 1.10476 2.42526 1.50352V1.52764H1.82376C1.34046 1.52891 0.877316 1.72195 0.53557 2.06455C0.193824 2.40716 0.00127018 2.87146 0 3.35598V14.1717C0.0016909 14.656 0.194379 15.1201 0.536035 15.4626C0.87769 15.8051 1.34059 15.9983 1.82376 16H14.1746C14.6581 15.9987 15.1214 15.8057 15.4634 15.4632C15.8054 15.1206 15.9983 14.6563 16 14.1717V3.35598C15.9987 2.87146 15.8062 2.40716 15.4644 2.06455C15.1227 1.72195 14.6595 1.52891 14.1762 1.52764ZM11.8889 1.50352C11.8889 1.4033 11.9286 1.30718 11.9993 1.23631C12.07 1.16544 12.1659 1.12563 12.2659 1.12563C12.3658 1.12563 12.4617 1.16544 12.5324 1.23631C12.6031 1.30718 12.6428 1.4033 12.6428 1.50352V2.99899C12.6428 3.09922 12.6031 3.19534 12.5324 3.2662C12.4617 3.33707 12.3658 3.37688 12.2659 3.37688C12.1659 3.37688 12.07 3.33707 11.9993 3.2662C11.9286 3.19534 11.8889 3.09922 11.8889 2.99899V1.50352ZM3.54807 1.50352C3.54807 1.4033 3.58778 1.30718 3.65847 1.23631C3.72916 1.16544 3.82504 1.12563 3.92501 1.12563C4.02498 1.12563 4.12086 1.16544 4.19155 1.23631C4.26224 1.30718 4.30195 1.4033 4.30195 1.50352V2.99899C4.30195 3.09922 4.26224 3.19534 4.19155 3.2662C4.12086 3.33707 4.02498 3.37688 3.92501 3.37688C3.82504 3.37688 3.72916 3.33707 3.65847 3.2662C3.58778 3.19534 3.54807 3.09922 3.54807 2.99899V1.50352ZM14.8772 14.1717C14.8747 14.3573 14.8001 14.5345 14.6691 14.6658C14.5382 14.797 14.3614 14.8719 14.1762 14.8744H1.82536C1.63995 14.8723 1.4627 14.7976 1.33144 14.6663C1.20018 14.5351 1.12531 14.3575 1.12281 14.1717V6.59296H14.8772V14.1717Z' fill='%23167CAC'/%3E%3Cpath d='M2.33063 11.282H3.93464V12.6006C3.93464 12.7499 3.99379 12.8931 4.09907 12.9986C4.20435 13.1041 4.34715 13.1634 4.49604 13.1634C4.64494 13.1634 4.78773 13.1041 4.89301 12.9986C4.9983 12.8931 5.05745 12.7499 5.05745 12.6006V11.282H7.46346V12.6006C7.46346 12.7499 7.52261 12.8931 7.62789 12.9986C7.73318 13.1041 7.87597 13.1634 8.02486 13.1634C8.17376 13.1634 8.31655 13.1041 8.42184 12.9986C8.52712 12.8931 8.58627 12.7499 8.58627 12.6006V11.282H10.9923V12.6006C10.9923 12.7499 11.0514 12.8931 11.1567 12.9986C11.262 13.1041 11.4048 13.1634 11.5537 13.1634C11.7026 13.1634 11.8454 13.1041 11.9507 12.9986C12.0559 12.8931 12.1151 12.7499 12.1151 12.6006V11.282H13.7191C13.868 11.282 14.0108 11.2227 14.1161 11.1172C14.2214 11.0116 14.2805 10.8685 14.2805 10.7192C14.2805 10.57 14.2214 10.4268 14.1161 10.3213C14.0108 10.2157 13.868 10.1564 13.7191 10.1564H12.1151V8.84425C12.1151 8.69498 12.0559 8.55183 11.9507 8.44628C11.8454 8.34073 11.7026 8.28143 11.5537 8.28143C11.4048 8.28143 11.262 8.34073 11.1567 8.44628C11.0514 8.55183 10.9923 8.69498 10.9923 8.84425V10.1628H8.58627V8.84425C8.58627 8.69498 8.52712 8.55183 8.42184 8.44628C8.31655 8.34073 8.17376 8.28143 8.02486 8.28143C7.87597 8.28143 7.73318 8.34073 7.62789 8.44628C7.52261 8.55183 7.46346 8.69498 7.46346 8.84425V10.1628H5.05745V8.84425C5.05745 8.69498 4.9983 8.55183 4.89301 8.44628C4.78773 8.34073 4.64494 8.28143 4.49604 8.28143C4.34715 8.28143 4.20435 8.34073 4.09907 8.44628C3.99379 8.55183 3.93464 8.69498 3.93464 8.84425V10.1628H2.33063C2.18174 10.1628 2.03894 10.2221 1.93366 10.3277C1.82837 10.4332 1.76923 10.5764 1.76923 10.7257C1.76923 10.8749 1.82837 11.0181 1.93366 11.1236C2.03894 11.2292 2.18174 11.2885 2.33063 11.2885V11.282Z' fill='%23167CAC'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: center;
width: 16px; // check this
height: 16px;
display: flex;
border-radius: 0 7px 7px 0;
background-color: #E4F1F7;
&:hover {
cursor: pointer;
}
padding: 15px 0;
min-width: 48px;
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 1px;
background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M14.1762 1.52764H13.7656V1.50352C13.7656 1.10476 13.6076 0.722334 13.3263 0.44037C13.0451 0.158406 12.6636 0 12.2659 0C11.8681 0 11.4866 0.158406 11.2054 0.44037C10.9241 0.722334 10.7661 1.10476 10.7661 1.50352V1.52764H5.42476V1.50352C5.42476 1.10476 5.26675 0.722334 4.9855 0.44037C4.70424 0.158406 4.32277 0 3.92501 0C3.52725 0 3.14579 0.158406 2.86453 0.44037C2.58327 0.722334 2.42526 1.10476 2.42526 1.50352V1.52764H1.82376C1.34046 1.52891 0.877316 1.72195 0.53557 2.06455C0.193824 2.40716 0.00127018 2.87146 0 3.35598V14.1717C0.0016909 14.656 0.194379 15.1201 0.536035 15.4626C0.87769 15.8051 1.34059 15.9983 1.82376 16H14.1746C14.6581 15.9987 15.1214 15.8057 15.4634 15.4632C15.8054 15.1206 15.9983 14.6563 16 14.1717V3.35598C15.9987 2.87146 15.8062 2.40716 15.4644 2.06455C15.1227 1.72195 14.6595 1.52891 14.1762 1.52764ZM11.8889 1.50352C11.8889 1.4033 11.9286 1.30718 11.9993 1.23631C12.07 1.16544 12.1659 1.12563 12.2659 1.12563C12.3658 1.12563 12.4617 1.16544 12.5324 1.23631C12.6031 1.30718 12.6428 1.4033 12.6428 1.50352V2.99899C12.6428 3.09922 12.6031 3.19534 12.5324 3.2662C12.4617 3.33707 12.3658 3.37688 12.2659 3.37688C12.1659 3.37688 12.07 3.33707 11.9993 3.2662C11.9286 3.19534 11.8889 3.09922 11.8889 2.99899V1.50352ZM3.54807 1.50352C3.54807 1.4033 3.58778 1.30718 3.65847 1.23631C3.72916 1.16544 3.82504 1.12563 3.92501 1.12563C4.02498 1.12563 4.12086 1.16544 4.19155 1.23631C4.26224 1.30718 4.30195 1.4033 4.30195 1.50352V2.99899C4.30195 3.09922 4.26224 3.19534 4.19155 3.2662C4.12086 3.33707 4.02498 3.37688 3.92501 3.37688C3.82504 3.37688 3.72916 3.33707 3.65847 3.2662C3.58778 3.19534 3.54807 3.09922 3.54807 2.99899V1.50352ZM14.8772 14.1717C14.8747 14.3573 14.8001 14.5345 14.6691 14.6658C14.5382 14.797 14.3614 14.8719 14.1762 14.8744H1.82536C1.63995 14.8723 1.4627 14.7976 1.33144 14.6663C1.20018 14.5351 1.12531 14.3575 1.12281 14.1717V6.59296H14.8772V14.1717Z' fill='%23167CAC'/%3E%3Cpath d='M2.33063 11.282H3.93464V12.6006C3.93464 12.7499 3.99379 12.8931 4.09907 12.9986C4.20435 13.1041 4.34715 13.1634 4.49604 13.1634C4.64494 13.1634 4.78773 13.1041 4.89301 12.9986C4.9983 12.8931 5.05745 12.7499 5.05745 12.6006V11.282H7.46346V12.6006C7.46346 12.7499 7.52261 12.8931 7.62789 12.9986C7.73318 13.1041 7.87597 13.1634 8.02486 13.1634C8.17376 13.1634 8.31655 13.1041 8.42184 12.9986C8.52712 12.8931 8.58627 12.7499 8.58627 12.6006V11.282H10.9923V12.6006C10.9923 12.7499 11.0514 12.8931 11.1567 12.9986C11.262 13.1041 11.4048 13.1634 11.5537 13.1634C11.7026 13.1634 11.8454 13.1041 11.9507 12.9986C12.0559 12.8931 12.1151 12.7499 12.1151 12.6006V11.282H13.7191C13.868 11.282 14.0108 11.2227 14.1161 11.1172C14.2214 11.0116 14.2805 10.8685 14.2805 10.7192C14.2805 10.57 14.2214 10.4268 14.1161 10.3213C14.0108 10.2157 13.868 10.1564 13.7191 10.1564H12.1151V8.84425C12.1151 8.69498 12.0559 8.55183 11.9507 8.44628C11.8454 8.34073 11.7026 8.28143 11.5537 8.28143C11.4048 8.28143 11.262 8.34073 11.1567 8.44628C11.0514 8.55183 10.9923 8.69498 10.9923 8.84425V10.1628H8.58627V8.84425C8.58627 8.69498 8.52712 8.55183 8.42184 8.44628C8.31655 8.34073 8.17376 8.28143 8.02486 8.28143C7.87597 8.28143 7.73318 8.34073 7.62789 8.44628C7.52261 8.55183 7.46346 8.69498 7.46346 8.84425V10.1628H5.05745V8.84425C5.05745 8.69498 4.9983 8.55183 4.89301 8.44628C4.78773 8.34073 4.64494 8.28143 4.49604 8.28143C4.34715 8.28143 4.20435 8.34073 4.09907 8.44628C3.99379 8.55183 3.93464 8.69498 3.93464 8.84425V10.1628H2.33063C2.18174 10.1628 2.03894 10.2221 1.93366 10.3277C1.82837 10.4332 1.76923 10.5764 1.76923 10.7257C1.76923 10.8749 1.82837 11.0181 1.93366 11.1236C2.03894 11.2292 2.18174 11.2885 2.33063 11.2885V11.282Z' fill='%23167CAC'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: center;
width: 16px; // check this
height: 16px;
display: flex;
border-radius: 0 7px 7px 0;
background-color: #e4f1f7;
&:hover {
cursor: pointer;
}
padding: 15px 0;
min-width: 48px;
}
.textbox-question {
label {
color: $black;
font-weight: 500;
}
span{
font-weight:400;
font-size:14px;
color: #4D5151;
}
.form-test-error span{
color: #d4281c;
font-size: 0.875rem;
font-weight: 500;
}
label {
color: $black;
font-weight: 500;
}
span {
font-weight: 400;
font-size: 14px;
color: #4d5151;
}
.form-test-error span {
color: #d4281c;
font-size: 0.875rem;
font-weight: 500;
}
.input-wrapper {
position: relative;
&.has-search-icon {
input[type="text"] {
border-radius: 50rem;
}
button[type="submit"] {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 0;
background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M15.7817 14.7328L11.8252 10.7762C12.8833 9.45005 13.3936 7.76911 13.2513 6.07849C13.1091 4.38788 12.325 2.81587 11.0601 1.6852C9.79515 0.554524 8.14538 -0.0490261 6.44946 -0.00154744C4.75353 0.0459312 3.14012 0.740836 1.94045 1.94051C0.740775 3.14018 0.0458701 4.75359 -0.00160848 6.44952C-0.0490871 8.14545 0.554463 9.79521 1.68514 11.0601C2.81581 12.325 4.38782 13.1091 6.07843 13.2514C7.76905 13.3937 9.44999 12.8834 10.7762 11.8252L14.7349 15.7839C14.8044 15.8527 14.8869 15.907 14.9774 15.9439C15.068 15.9808 15.165 15.9995 15.2628 15.9989C15.3606 15.9983 15.4573 15.9784 15.5475 15.9405C15.6376 15.9025 15.7194 15.8471 15.7881 15.7776C15.8568 15.708 15.9112 15.6256 15.9481 15.535C15.985 15.4444 16.0036 15.3474 16.0031 15.2496C16.0025 15.1518 15.9826 15.0551 15.9446 14.965C15.9067 14.8748 15.8513 14.7931 15.7817 14.7243V14.7328ZM6.63737 11.7913C5.61803 11.7913 4.62157 11.4891 3.77402 10.9228C2.92646 10.3564 2.26587 9.5515 1.87578 8.60975C1.4857 7.668 1.38363 6.63172 1.5825 5.63196C1.78136 4.6322 2.27222 3.71386 2.99301 2.99307C3.7138 2.27229 4.63214 1.78142 5.6319 1.58256C6.63166 1.38369 7.66793 1.48576 8.60969 1.87585C9.55144 2.26593 10.3564 2.92652 10.9227 3.77408C11.489 4.62163 11.7913 5.61809 11.7913 6.63743C11.7896 8.00382 11.2461 9.31376 10.2799 10.2799C9.3137 11.2461 8.00376 11.7897 6.63737 11.7913Z' fill='%231574A1'/%3E%3C/svg%3E%0A");
background-repeat: no-repeat;
background-position: center;
border-radius: 0 50rem 50rem 0;
background-color: $blue-100;
width: 2.75rem;
height: 100%;
border: 1px solid $gray-500;
border-left: none;
display: flex;
}
}
&.has-select-icon {
button[type="submit"] {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 1rem;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 8.89' xml:space='preserve'%3e%3cpath d='M8 8.89c-.24 0-.46-.09-.63-.26L.26 1.53a.901.901 0 0 1 0-1.27C.43.1.66 0 .9 0s.47.1.64.26L8 6.74 14.47.27c.17-.17.4-.27.64-.27s.47.1.63.27c.17.17.26.4.26.64s-.1.47-.27.63l-7.1 7.09a.86.86 0 0 1-.63.26z' fill='%231474a2'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: center;
background-color: transparent;
width: 1rem;
height: 100%;
border: 0px;
border-left: none;
display: flex;
}
}
}
input {
&.has-icon {
background-image: url(~@/assets/img/icons/location-pin.svg);
background-repeat: no-repeat;
background-position: 0.75rem 50%;
background-size: 1rem auto;
padding: 0 0.75rem 0 2.5rem;
&.icon-right {
background-position: calc(100% - 0.75rem) 50%;
padding: 0 2.5rem 0 0.75rem;
}
}
}
.form-label {
margin-bottom: 0.25rem;
}
.form-control {
.input-wrapper {
position: relative;
&.has-search-icon {
input[type="text"] {
border-radius: 50rem;
}
button[type="submit"] {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 0;
background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M15.7817 14.7328L11.8252 10.7762C12.8833 9.45005 13.3936 7.76911 13.2513 6.07849C13.1091 4.38788 12.325 2.81587 11.0601 1.6852C9.79515 0.554524 8.14538 -0.0490261 6.44946 -0.00154744C4.75353 0.0459312 3.14012 0.740836 1.94045 1.94051C0.740775 3.14018 0.0458701 4.75359 -0.00160848 6.44952C-0.0490871 8.14545 0.554463 9.79521 1.68514 11.0601C2.81581 12.325 4.38782 13.1091 6.07843 13.2514C7.76905 13.3937 9.44999 12.8834 10.7762 11.8252L14.7349 15.7839C14.8044 15.8527 14.8869 15.907 14.9774 15.9439C15.068 15.9808 15.165 15.9995 15.2628 15.9989C15.3606 15.9983 15.4573 15.9784 15.5475 15.9405C15.6376 15.9025 15.7194 15.8471 15.7881 15.7776C15.8568 15.708 15.9112 15.6256 15.9481 15.535C15.985 15.4444 16.0036 15.3474 16.0031 15.2496C16.0025 15.1518 15.9826 15.0551 15.9446 14.965C15.9067 14.8748 15.8513 14.7931 15.7817 14.7243V14.7328ZM6.63737 11.7913C5.61803 11.7913 4.62157 11.4891 3.77402 10.9228C2.92646 10.3564 2.26587 9.5515 1.87578 8.60975C1.4857 7.668 1.38363 6.63172 1.5825 5.63196C1.78136 4.6322 2.27222 3.71386 2.99301 2.99307C3.7138 2.27229 4.63214 1.78142 5.6319 1.58256C6.63166 1.38369 7.66793 1.48576 8.60969 1.87585C9.55144 2.26593 10.3564 2.92652 10.9227 3.77408C11.489 4.62163 11.7913 5.61809 11.7913 6.63743C11.7896 8.00382 11.2461 9.31376 10.2799 10.2799C9.3137 11.2461 8.00376 11.7897 6.63737 11.7913Z' fill='%231574A1'/%3E%3C/svg%3E%0A");
background-repeat: no-repeat;
background-position: center;
border-radius: 0 50rem 50rem 0;
background-color: $blue-100;
width: 2.75rem;
height: 100%;
border: 1px solid $gray-500;
border-radius: 0.5rem;
min-height: 3rem;
max-height: 48px;
padding: 12px 16px;
&::placeholder {
color: $gray-500;
}
&:focus {
box-shadow: 0 0 0 2.5px $blue;
}
&:disabled,
&.disabled {
background-color: $gray-100;
&:hover {
box-shadow: 0 0 0 4px transparent;
border: 1px solid $gray-500;
}
}
&:hover {
border: 1px solid $gray-500;
box-shadow: 0 0 0 4px $blue-300;
}
border-left: none;
display: flex;
}
}
p {
display: none;
&.has-select-icon {
button[type="submit"] {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 1rem;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 8.89' xml:space='preserve'%3e%3cpath d='M8 8.89c-.24 0-.46-.09-.63-.26L.26 1.53a.901.901 0 0 1 0-1.27C.43.1.66 0 .9 0s.47.1.64.26L8 6.74 14.47.27c.17-.17.4-.27.64-.27s.47.1.63.27c.17.17.26.4.26.64s-.1.47-.27.63l-7.1 7.09a.86.86 0 0 1-.63.26z' fill='%231474a2'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: center;
background-color: transparent;
width: 1rem;
height: 100%;
border: 0px;
border-left: none;
display: flex;
}
}
}
input {
&.has-icon {
background-image: url(~@/assets/img/icons/location-pin.svg);
background-repeat: no-repeat;
background-position: 0.75rem 50%;
background-size: 1rem auto;
padding: 0 0.75rem 0 2.5rem;
&.icon-right {
background-position: calc(100% - 0.75rem) 50%;
padding: 0 2.5rem 0 0.75rem;
}
}
}
.form-label {
margin-bottom: 0.25rem;
}
.form-control {
border: 1px solid $gray-500;
border-radius: 0.5rem;
min-height: 3rem;
max-height: 48px;
padding: 12px 16px;
&::placeholder {
color: $gray-500;
}
&:focus {
box-shadow: 0 0 0 2.5px $blue;
}
&:disabled,
&.disabled {
background-color: $gray-100;
&:hover {
box-shadow: 0 0 0 4px transparent;
border: 1px solid $gray-500;
}
}
&:hover {
border: 1px solid $gray-500;
box-shadow: 0 0 0 4px $blue-300;
}
}
p {
display: none;
}
}
</style>

View file

@ -0,0 +1,47 @@
import { defineRule } from 'vee-validate';
import { errorMessages } from '@/constants/error-messages';
import globalRules from '@/constants/global-rules';
import { required, regex } from '@/helpers/validation-rules';
/**
* @summary Define global rules related to names
*/
function defineGlobalNameRules() {
defineRule(globalRules.FIRST_NAME_REQUIRED, required(errorMessages.FIRST_NAME_REQUIRED));
defineRule(globalRules.LAST_NAME_REQUIRED, required(errorMessages.LAST_NAME_REQUIRED));
defineRule(globalRules.POLICYHOLDER_FIRST_NAME_REQUIRED,
required(errorMessages.POLICYHOLDER_FIRST_NAME_REQUIRED));
defineRule(globalRules.POLICYHOLDER_LAST_NAME_REQUIRED,
required(errorMessages.POLICYHOLDER_LAST_NAME_REQUIRED));
}
/**
* @summary Define global rules related to email addresses
*/
function defineGlobalEmailRules() {
defineRule(globalRules.EMAIL_ADDRESS_REQUIRED, required(errorMessages.EMAIL_ADDRESS_REQUIRED));
defineRule(globalRules.EMAIL_ADDRESS_FORMAT,
regex(/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9_\-.]+)\.([a-zA-Z]{2,})$/,
errorMessages.EMAIL_ADDRESS_FORMAT));
}
/**
* @summary Define global rules related to phone numbers
*/
function defineGlobalPhoneNumberRules() {
defineRule(globalRules.PHONE_NUMBER_REQUIRED, required(errorMessages.PHONE_NUMBER_REQUIRED));
defineRule(globalRules.PHONE_NUMBER_FORMAT,
regex(/^(\([0-9]{3}\)|[0-9]{3}) *[-.]? *[0-9]{3} *[-.]? *[0-9]{4}$/,
errorMessages.PHONE_NUMBER_FORMAT));
}
/**
* @summary Define all global rules
*/
export default function defineGlobalRules() {
defineGlobalNameRules();
defineGlobalEmailRules();
defineGlobalPhoneNumberRules();
defineRule(globalRules.OPTION_REQUIRED, required(errorMessages.OPTION_REQUIRED));
}

View file

@ -8,7 +8,7 @@
ref="firstName"
inputId="08497a2efd9a4a73a70360ab47b4838d"
disableAutoFill
validationRules="first-name-required" />
:validationRules="rules.firstName" />
</div>
</div>
<div class="row mb-4">
@ -19,21 +19,15 @@
ref="lastName"
inputId="0030e56a57e74a4ab92de7fb8e97fec5"
disableAutoFill
validationRules="last-name-required" />
:validationRules="rules.lastName" />
</div>
</div>
</template>
<script>
import addressQuestions from "@/iss-components/address-questions/address-questions";
import textboxQuestion from "@/digital-components/textbox-question/textbox-question";
import { defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
// DEFINE VALIDATION RULES
defineRule("first-name-required", required(errorMessages.FIRST_NAME_REQUIRED));
defineRule("last-name-required", required(errorMessages.LAST_NAME_REQUIRED));
import addressQuestions from '@/iss-components/address-questions/address-questions.vue';
import textboxQuestion from '@/digital-components/textbox-question/textbox-question.vue';
import globalRules from '@/constants/global-rules';
export default {
name: "customer-questions",
@ -56,6 +50,14 @@ export default {
},
validationRules: String,
},
data() {
return {
rules: {
firstName: globalRules.FIRST_NAME_REQUIRED,
lastName: globalRules.LAST_NAME_REQUIRED
}
};
},
computed: {
customerModel: {
get: function () {

View file

@ -11,7 +11,7 @@
:alertFewMoreQuestionsHeader="AlertFewMoreQuestionsHeader"
:alertFewMoreQuestionsCopy="AlertFewMoreQuestionsCopy"
:questionsData="questionsData"
validationRules="questions-required"
:validationRules="rules.optionRequired"
v-model="selectedAnswers"
@forwardButtonAction="forwardButtonAction"
@back-click="navigateBack"
@ -26,14 +26,11 @@ import { settleAllPromises } from '@/helpers/layout-helper';
// Import Component
import baseFormMixin from '@/mixins/base-form-mixin';
import { issPageValues } from "@/router/router-constants/issPage-values";
import { Form, defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
import { Form } from "vee-validate";
import { useMainStore } from "@/store";
import { errorMessages } from "@/constants/error-messages";
import globalRules from '@/constants/global-rules';
import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin";
import questionsPageLayout from "@/iss-components/questions-page-layout/questions-page-layout.vue";
// DEFINE VALIDATION RULES
defineRule("questions-required", required(errorMessages.OPTION_REQUIRED));
export default {
name: 'capability-questions',
@ -45,8 +42,11 @@ export default {
data() {
return {
questionsData: [],
selectedAnswers: {},
currentGlassIndex: 0,
selectedAnswers: {},
currentGlassIndex: 0,
rules: {
optionRequired: globalRules.OPTION_REQUIRED
}
};
},
async beforeRouteEnter(to, from, next) {

View file

@ -0,0 +1,226 @@
// Components
import contactDetails from '@/layouts/contact-details/contact-details.vue';
// Supporting Files
import { shallowMount } from '@vue/test-utils';
import { getMountOptions } from '@/helpers/unit-test-helper';
import { getRandomString, getRandomInt } from '@/helpers/data-generation';
import { createTestingPinia } from '@pinia/testing';
import { navigationScenarios } from '@/router/router-constants/navigation-scenarios';
describe('contactDetails.vue', () => {
describe('Rendering', () => {
test('Should render site header', () => {
// Arrange
const wrapper = shallowMount(contactDetails, getMountOptions());
// Act
const siteHeader = wrapper.findComponent({ ref: 'siteHeader' });
// Assert
expect(siteHeader.exists()).toBe(true);
});
test('Should render sub title', () => {
// Arrange
const wrapper = shallowMount(contactDetails, getMountOptions());
// Act
const siteSubHeader = wrapper.findComponent({ ref: 'siteSubHeader' });
// Assert
expect(siteSubHeader.exists()).toBe(true);
});
test('Should render first name question subcomponent', () => {
// Arrange
const wrapper = shallowMount(contactDetails, getMountOptions());
// Act
const firstNameQuestion = wrapper.findComponent({ ref: 'firstNameQuestion' });
// Assert
expect(firstNameQuestion.exists()).toBe(true);
});
test('Should render last name question subcomponent', () => {
// Arrange
const wrapper = shallowMount(contactDetails, getMountOptions());
// Act
const lastNameQuestion = wrapper.findComponent({ ref: 'lastNameQuestion' });
// Assert
expect(lastNameQuestion.exists()).toBe(true);
});
test('Should render email question subcomponent', () => {
// Arrange
const wrapper = shallowMount(contactDetails, getMountOptions());
// Act
const emailQuestion = wrapper.findComponent({ ref: 'emailQuestion' });
// Assert
expect(emailQuestion.exists()).toBe(true);
});
test('Should render phone number question subcomponent', () => {
// Arrange
const wrapper = shallowMount(contactDetails, getMountOptions());
// Act
const phoneNumberQuestion = wrapper.findComponent({ ref: 'phoneNumberQuestion' });
// Assert
expect(phoneNumberQuestion.exists()).toBe(true);
});
test('Should render text updates checkbox subcomponent', () => {
// Arrange
const checkboxLabel = getRandomString(50, 100);
const mockMixin = {
methods: {
getCmsContent: jest.fn().mockImplementation(() =>
checkboxLabel),
setCmsContent: jest.fn()
}
};
const mountOptions = getMountOptions();
mountOptions.mixins = [mockMixin];
const wrapper = shallowMount(contactDetails, mountOptions);
// Act
const textUpdatesCheckbox = wrapper.findComponent({ ref: 'requestTextUpdatesCheckbox' });
// Assert
expect(textUpdatesCheckbox.exists()).toBe(true);
expect(wrapper.vm.requestTextUpdatesCheckboxText).toBe(`${checkboxLabel}*`);
});
test('Should render technician notes textarea question subcomponent', () => {
// Arrange
const wrapper = shallowMount(contactDetails, getMountOptions());
// Act
const notesQuestion = wrapper.findComponent({ ref: 'notesQuestion' });
// Assert
expect(notesQuestion.exists()).toBe(true);
});
test('Should render disclaimer text', () => {
// Arrange
const disclaimerText = getRandomString(50, 100);
const mockMixin = {
methods: {
getCmsContent: jest.fn().mockImplementation(() =>
disclaimerText),
setCmsContent: jest.fn()
}
};
const mountOptions = getMountOptions();
mountOptions.mixins = [mockMixin];
const wrapper = shallowMount(contactDetails, mountOptions);
const expectedDisclaimerText = `*${disclaimerText} I also agree to Safelite's`;
// Act
const componentText = wrapper.text();
// Assert
expect(wrapper.vm.textUpdateDisclaimerText).toBe(`*${disclaimerText}`);
expect(componentText).toContain(expectedDisclaimerText);
});
test('Should render privacy policy link', () => {
// Arrange
const wrapper = shallowMount(contactDetails, getMountOptions());
// Act
const privacyPolicyLink = wrapper.findComponent({ ref: 'privacyPolicyLink' });
// Assert
expect(privacyPolicyLink.exists()).toBe(true);
});
test('Should render terms of use link', () => {
// Arrange
const wrapper = shallowMount(contactDetails, getMountOptions());
// Act
const termsOfUseLink = wrapper.findComponent({ ref: 'termsOfUseLink' });
// Assert
expect(termsOfUseLink.exists()).toBe(true);
});
test('Should render site footer', () => {
// Arrange
const wrapper = shallowMount(contactDetails, getMountOptions());
// Act
const footer = wrapper.findComponent({ ref: 'siteFooter' });
// Assert
expect(footer.exists()).toBe(true);
});
test('Mocked store yields expected data', () => {
// Arrange
const mountOptions = getMountOptions();
const firstName = getRandomString(4, 15);
const lastName = getRandomString(4, 15);
const emailAddress = getRandomString(10, 20);
const phoneNumber = getRandomInt(1000000000, 9999999999);
const mainInitialState = {
order: {
customer: {
firstName,
lastName,
emailAddress,
phoneNumber
}
}
};
mountOptions.global = {
plugins: [createTestingPinia({
initialState: {
main: mainInitialState
}
})]
};
const wrapper = shallowMount(contactDetails, mountOptions);
// Assert
expect(wrapper.vm.firstName).toBe(firstName);
expect(wrapper.vm.lastName).toBe(lastName);
expect(wrapper.vm.email).toBe(emailAddress);
expect(wrapper.vm.phoneNumber).toBe(phoneNumber);
});
});
describe('Navigation', () => {
test('Back button clicked triggers navigation', () => {
// Arrange
const wrapper = shallowMount(contactDetails, getMountOptions({
router: {
navigate: jest.fn()
}
}));
// Act
wrapper.vm.backButtonAction();
// Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalled();
expect(wrapper.vm.$router.navigate)
.toHaveBeenCalledWith(navigationScenarios.CLICKED_BACK, undefined);
});
test('Forward button clicked triggers navigation', () => {
// Arrange
const wrapper = shallowMount(contactDetails, getMountOptions({
router: {
navigate: jest.fn()
}
}));
// Act
wrapper.vm.forwardButtonAction();
// Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalled();
expect(wrapper.vm.$router.navigate)
.toHaveBeenCalledWith(navigationScenarios.CLICKED_FORWARD, undefined);
});
});
});

View file

@ -1,17 +1,96 @@
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }" >
<Form
ref="contact-details-form"
v-slot="{ meta }"
@submit="onSubmit"
@invalidSubmit="onInvalidSubmit">
<div class="page-container-grouped-styles">
<div class="fade-on-route-transition position-relative">
<siteHeader cmsWidgetName="SiteHeaderWidget"/>
<siteHeader
ref="siteHeader"
:cmsWidgetName="widget.siteHeader" />
<div class="container-fluid pb-2">
<p>Placeholder for contact details page</p>
<siteSubHeader
id="sub-header"
ref="siteSubHeader"
class="margin-top-16"
:cmsWidgetName="widget.siteSubHeader" />
<textboxQuestion
ref="firstNameQuestion"
v-model="firstName"
class="margin-top-24"
inputId="firstName"
:cmsWidgetName="widget.firstNameQuestion"
isRequired
:validationRules="rules.firstName" />
<textboxQuestion
ref="lastNameQuestion"
v-model="lastName"
class="margin-top-16"
inputId="lastName"
:cmsWidgetName="widget.lastNameQuestion"
isRequired
:validationRules="rules.lastName" />
<textboxQuestion
ref="emailQuestion"
v-model="email"
class="margin-top-16"
inputId="email"
:cmsWidgetName="widget.emailQuestion"
isRequired
:validationRules="rules.email" />
<textboxQuestion
ref="phoneNumberQuestion"
v-model="phoneNumber"
class="margin-top-16"
inputId="phoneNumber"
:cmsWidgetName="widget.phoneNumberQuestion"
isRequired
:validationRules="rules.phoneNumber" />
<checkbox
ref="requestTextUpdatesCheckbox"
v-model="requestTextUpdates"
class="margin-top-8"
checkboxName="requestTextUpdates"
buttonID="requestTextUpdates"
:checkboxLabel="requestTextUpdatesCheckboxText" />
<textareaQuestion
ref="notesQuestion"
v-model="notesForTechnician"
class="margin-top-16"
inputId="technicianNotes"
:isDisabled="false"
:isRequired="false"
:cmsWidgetName="widget.notesQuestion"
maxLength="500"
:inputRows="3" />
<p
ref="disclaimerText"
class="disclaimer margin-top-24">
{{ textUpdateDisclaimerText }} I also agree to Safelite's
<textLink
ref="privacyPolicyLink"
class="disclaimer-link"
linkType="text"
text="Privacy Policy"
href="//www.safelite.com/privacy-center" />
and
<textLink
ref="termsOfUseLink"
class="disclaimer-link"
linkType="text"
text="Terms of Use"
href="//www.safelite.com/terms-of-use" />.
</p>
<siteFooter
cmsWidgetName="SiteFooterWidget"
ref="siteFooter"
:isForwardActionDisabled="!meta.valid"
@ForwardClicked="navigateForward"
@back-clicked="backButtonAction"
/>
ref="siteFooter"
:cmsWidgetName="widget.siteFooter"
:isForwardActionDisabled="!meta.valid"
@forwardClicked="navigateForward"
@backClicked="backButtonAction" />
</div>
</div>
</div>
@ -19,67 +98,131 @@
</template>
<script>
// Components
import siteHeader from '@/iss-components/site-header/site-header';
import siteFooter from "@/iss-components/site-footer/site-footer";
import siteHeader from '@/iss-components/site-header/site-header.vue';
import siteFooter from '@/iss-components/site-footer/site-footer.vue';
import siteSubHeader from '@/iss-components/site-sub-header/site-sub-header.vue';
import textboxQuestion from '@/digital-components/textbox-question/textbox-question.vue';
import checkbox from '@/ux-components/checkbox/checkbox.vue';
import textareaQuestion from '@/digital-components/textarea-question/textarea-question.vue';
import textLink from '@/ux-components/text-link/text-link.vue';
// Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { Form } from "vee-validate";
import BaseFormMixin from '@/mixins/base-form-mixin.js';
import { fetchCmsContentForPage } from '@/helpers/cms-content-helper';
import { Form } from 'vee-validate';
import BaseFormMixin from '@/mixins/base-form-mixin';
import { useMainStore } from '@/store';
import globalRules from '@/constants/global-rules';
export default {
name: "contact-details",
name: 'contact-details',
components: {
siteHeader,
siteSubHeader,
textboxQuestion,
checkbox,
textareaQuestion,
siteFooter,
// eslint-disable-next-line vue/no-reserved-component-names
Form,
textLink
},
mixins: [BaseFormMixin],
async beforeRouteEnter(to, from, next) {
const cmsContent = await fetchCmsContentForPage(to.query.issPage);
next((vm) => {
vm.setCmsContent(cmsContent);
});
},
data() {
return {
firstName: useMainStore().order.customer.firstName,
lastName: useMainStore().order.customer.lastName,
email: useMainStore().order.customer.emailAddress,
phoneNumber: useMainStore().order.customer.phoneNumber,
requestTextUpdates: false,
notesForTechnician: '',
widget: {
siteHeader: 'SiteHeaderWidget',
siteSubHeader: 'SiteSubHeaderWidget',
firstNameQuestion: 'FirstNameQuestionWidget',
lastNameQuestion: 'LastNameQuestionWidget',
emailQuestion: 'EmailQuestionWidget',
phoneNumberQuestion: 'PhoneNumberQuestionWidget',
requestTextUpdates: 'TextContentWidget',
notesQuestion: 'NotesQuestionWidget',
disclaimer: 'TextUpdateDisclaimerWidget',
siteFooter: 'SiteFooterWidget'
},
rules: {
firstName: globalRules.FIRST_NAME_REQUIRED,
lastName: globalRules.LAST_NAME_REQUIRED,
email: `${globalRules.EMAIL_ADDRESS_REQUIRED}|${globalRules.EMAIL_ADDRESS_FORMAT}`,
phoneNumber: `${globalRules.PHONE_NUMBER_REQUIRED}|${globalRules.PHONE_NUMBER_FORMAT}`
}
};
},
setup() {
const mainStore = useMainStore();
return { mainStore };
computed: {
/**
* @summary Returns the CMS text associated with the "get text updates" checkbox.
*/
requestTextUpdatesCheckboxText() {
return `${this.getCmsContent(this.widget.requestTextUpdates, 'Text')}*`;
},
/**
* @summary Returns the CMS text associated with the "get text updates" checkbox.
*/
textUpdateDisclaimerText() {
return `*${this.getCmsContent(this.widget.disclaimer, 'Text')}`;
}
},
async beforeRouteEnter(to, from, next)
methods:
{
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
// Settle promises and get results
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},];
//use resultMap to populate layout content.
let resultMap = await settleAllPromises(promiseResultMap);
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
});
},
methods:
{
backButtonAction() {
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
},
forwardButtonAction() {
},
navigateForward() {
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD,
this.$route
);
},
},
components: {
siteHeader,
siteFooter,
Form,
/**
* @summary Steps to perform when back button clicked.
*/
backButtonAction() {
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
},
/**
* @summary Steps to perform when forward button clicked.
*/
forwardButtonAction() {
this.$router.navigate(this.navigationScenarios.CLICKED_FORWARD,
this.$route);
}
}
};
</script>
<style lang="scss">
<style lang="scss" scoped>
#sub-header {
span {
color: $black;
}
}
.margin-top-24 {
margin-top: 24px;
}
.margin-top-16 {
margin-top: 16px;
}
.margin-top-8 {
margin-top: 8px;
}
.disclaimer {
font-size: 12px;
color: $darker-gray;
font-weight: 400;
}
.disclaimer-link {
text-decoration: none;
line-height: normal;
}
</style>

View file

@ -12,7 +12,7 @@
:alertFewMoreQuestionsHeader="AlertFewMoreQuestionsHeader"
:alertFewMoreQuestionsCopy="AlertFewMoreQuestionsCopy"
:questionsData="questionsData"
validationRules="questions-required"
:validationRules="rules.optionRequired"
v-model="selectedAnswers"
@forwardButtonAction="forwardButtonAction"
@back-click="navigateBack"
@ -25,9 +25,7 @@
// Import Supporting Files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
import globalRules from '@/constants/global-rules';
import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin";
import { useMainStore } from "@/store";
import { issPageValues } from "@/router/router-constants/issPage-values";
@ -37,9 +35,6 @@ import BaseFormMixin from '@/mixins/base-form-mixin.js';
import { Form } from "vee-validate";
import questionsPageLayout from "@/iss-components/questions-page-layout/questions-page-layout.vue";
// DEFINE VALIDATION RULES
defineRule("questions-required", required(errorMessages.OPTION_REQUIRED));
export default {
name: "molding-questions",
mixins: [BaseFormMixin, vehicleQuestionsMixin],
@ -65,6 +60,9 @@ export default {
questionsData: [],
selectedAnswers: {},
currentGlassIndex: 0,
rules: {
optionRequired: globalRules.OPTION_REQUIRED
}
};
},
computed: {

View file

@ -6,7 +6,7 @@
:alertFewMoreQuestionsHeader="AlertFewMoreQuestionsHeader"
:alertFewMoreQuestionsCopy="AlertFewMoreQuestionsCopy"
:questionsData="questionsData"
validationRules="questions-required"
:validationRules="rules.optionRequired"
v-model="selectedAnswers"
@forwardButtonAction="forwardButtonAction"
@back-click="navigateBack"
@ -23,14 +23,10 @@ import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { useMainStore } from '@/store';
import { issPageValues } from "@/router/router-constants/issPage-values";
import { Form, defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
import { Form } from "vee-validate";
import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin";
import BaseFormMixin from '@/mixins/base-form-mixin.js';
// DEFINE VALIDATION RULES
defineRule("questions-required", required(errorMessages.OPTION_REQUIRED));
import globalRules from '@/constants/global-rules';
export default {
name: "part-questions",
@ -59,6 +55,9 @@ export default {
questionsData: [],
selectedAnswers: {},
currentGlassIndex: 0,
rules: {
optionRequired: globalRules.OPTION_REQUIRED
}
};
},
computed: {

View file

@ -15,7 +15,7 @@
isRequired
ref="policyHolderFirstName"
disableAutoFill
validationRules="first-name-required" />
:validationRules="rules.firstName" />
<textboxQuestion
class="mt-4"
inputId="lastNameField"
@ -24,7 +24,7 @@
isRequired
ref="policyHolderLastName"
disableAutoFill
validationRules="last-name-required" />
:validationRules="rules.lastName" />
</div>
</div>
<siteFooter
@ -51,16 +51,11 @@ import siteFooter from "@/iss-components/site-footer/site-footer";
// Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { Form, defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
import { Form } from "vee-validate";
import globalRules from '@/constants/global-rules';
import BaseFormMixin from '@/mixins/base-form-mixin.js';
import { useMainStore } from '@/store';
//define validation rules
defineRule("first-name-required", required(errorMessages.POLICYHOLDER_FIRST_NAME_REQUIRED));
defineRule("last-name-required", required(errorMessages.POLICYHOLDER_LAST_NAME_REQUIRED));
export default {
name: "policy-holder-details",
mixins: [BaseFormMixin],
@ -68,6 +63,10 @@ export default {
return {
customerQuestions: this.getPolicyHolderDetailsFromStore(),
vehiclesFound: [],
rules: {
firstName: globalRules.POLICYHOLDER_FIRST_NAME_REQUIRED,
lastName: globalRules.POLICYHOLDER_LAST_NAME_REQUIRED
}
};
},
setup() {

View file

@ -23,7 +23,7 @@
class="px-4"
cmsWidgetName="PolicyVehiclesQuestion"
:vehicles="VehiclesForQuestions"
validationRules="option-required" />
:validationRules="rules.optionRequired" />
<siteFooter
ref="siteFooter"
cmsWidgetName="SiteFooterWidget"
@ -47,17 +47,14 @@ import vehicleBanner from '@/iss-components/vehicle-banner/vehicle-banner.vue';
import policyVehiclesQuestion from '@/layouts/policy-vehicles/policy-vehicles-question/policy-vehicles-question.vue';
// Supporting files
import { fetchCmsContentForPage } from '@/helpers/cms-content-helper';
import { required } from '@/helpers/validation-rules';
import { Form, defineRule } from 'vee-validate';
import { Form } from 'vee-validate';
import BaseFormMixin from '@/mixins/base-form-mixin';
import { issPageValues } from '@/router/router-constants/issPage-values';
import { useMainStore } from '@/store';
import { errorMessages } from '@/constants/error-messages';
import { vehicleSelectionOptions } from '@/constants/vehicle-selection-options';
import { endorsementOptions } from '@/constants/endorsement-options';
import globalRules from '@/constants/global-rules';
// DEFINE VALIDATION RULES
defineRule('option-required', required(errorMessages.OPTION_REQUIRED));
export default {
name: 'policy-vehicles',
components: {
@ -83,7 +80,10 @@ export default {
? policyVehicles[0].vin
: '',
displayGeneric: (policyVehicles?.length ?? 0) !== 1,
bailout: false
bailout: false,
rules: {
optionRequired: globalRules.OPTION_REQUIRED
}
};
},
computed: {

View file

@ -11,7 +11,7 @@
groupName="prefQuestions"
buttonTypeString="providerPrefRadio"
v-model="selectedProvider"
validationRules="option-required"
:validationRules="rules.optionRequired"
isRequired
class="mx-5" />
<div class="px-5">
@ -44,7 +44,6 @@
// Import Supporting Files
import { fetchCmsContentForPage, setupModalLinks, processIfStatements } from '@/helpers/cms-content-helper';
import { settleAllPromises } from "@/helpers/layout-helper";
import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
import buttonQuestion from "@/digital-components/button-question/button-question";
import contentGroupModal from "@/iss-components/content-group-modal/content-group-modal";
@ -52,7 +51,7 @@ import { issPageValues } from "@/router/router-constants/issPage-values";
// Import Component
import baseFormMixin from "@/mixins/base-form-mixin";
import { Form, defineRule } from "vee-validate";
import { Form } from "vee-validate";
import siteFooter from "@/iss-components/site-footer/site-footer";
import siteHeader from "@/iss-components/site-header/site-header";
import siteSubHeader from "@/iss-components/site-sub-header/site-sub-header";
@ -61,9 +60,8 @@ import recalModal from "@/layouts/coverage-statement/recal-modal/recal-modal";
import steeringModal from "@/layouts/provider-preference/steering-modal/steering-modal";
import shopPreferenceModal from "@/layouts/provider-preference/shop-preference-modal/shop-preference-modal";
import tpaRecalModal from "@/layouts/provider-preference/tpa-recal-modal/tpa-recal-modal.vue"
import globalRules from '@/constants/global-rules';
// DEFINE VALIDATION RULES
defineRule('option-required', required(errorMessages.OPTION_REQUIRED));
const options = { SAFELITE: 'SafeliteOption', TPA: 'TPAOption' };
export default {
@ -88,6 +86,9 @@ export default {
selectedProvider : null,
showSteeringLink: false,
tpaAcknowledgement: false,
rules: {
optionRequired: globalRules.OPTION_REQUIRED
}
};
},
async beforeRouteEnter(to, from, next) {

View file

@ -13,7 +13,7 @@
<checkBox
class="mb-2"
:class="showError && ' has-error'"
validationRules="option-required"
:validationRules="rules.optionRequired"
checkboxName="tpaAcknowledgement"
buttonID="tpaAcknowledgement"
:tabIndex="0"
@ -35,6 +35,7 @@
import modal from "@/digital-components/modal/modal";
import tpaRecalToggle from "@/layouts/provider-preference/tpa-recal-modal/tpa-recal-toggle/tpa-recal-toggle.vue"
import checkBox from "@/ux-components/checkbox/checkbox.vue"
import globalRules from "@/constants/global-rules";
export default {
name: "content-group-modal",
@ -45,7 +46,10 @@ export default {
data() {
return {
acknowledged: false,
showError: false
showError: false,
rules: {
optionRequired: globalRules.OPTION_REQUIRED
}
};
},
emits: ["buttonClick"],

View file

@ -14,7 +14,7 @@
:availableLineItems="availableLineItems"
@vapsItemsSelected="vapsItemsSelectedAction"
v-on:link-event="openModalAction"
validationRules="option-required"
:validationRules="rules.optionRequired"
isRequired
/>
<p class="caption disclaimer" v-html="PriceDisclaimerText"></p>
@ -52,8 +52,7 @@ import { required } from '@/helpers/validation-rules';
import { errorMessages } from '@/constants/error-messages';
import { Form, defineRule } from 'vee-validate';
import { allGlassPartsAndItemsHavePrices } from '@/layouts/service-packages/service-package-helper/service-package-helper'
defineRule('option-required', required(errorMessages.OPTION_REQUIRED));
import globalRules from "@/constants/global-rules";
const store = useMainStore();
@ -112,7 +111,10 @@ export default {
selectedVaps: [],
availableLineItems: [],
supportingItems: [],
pricedGlassParts: []
pricedGlassParts: [],
rules: {
optionRequired: globalRules.OPTION_REQUIRED
}
};
},
computed: {

View file

@ -8,6 +8,10 @@ import {vinLookupMethodSelections} from '@/constants/vin-lookup-methods';
import { useMainStore } from '@/store';
import { createTestingPinia } from '@pinia/testing';
import { mapStores } from "pinia";
import { defineRule } from 'vee-validate';
import { errorMessages } from '@/constants/error-messages';
import globalRules from '@/constants/global-rules';
import { required } from '@/helpers/validation-rules';
const pinia = createTestingPinia();
useMainStore(pinia);
@ -44,6 +48,7 @@ const siteFooterWidgetMockData = {
};
function setupMocks() {
defineRule(globalRules.OPTION_REQUIRED, required(errorMessages.OPTION_REQUIRED));
const mockRoute = {
query: {
issPage: 'vehicle-lookup',

View file

@ -5,7 +5,7 @@
button-type-string="listButton"
:question-text="questionTextFromCms"
is-required
validation-rules="option-required"
:validation-rules="rules.optionRequired"
data-test-id="vin-lookup-methods-button-question"
/>
</template>
@ -13,32 +13,30 @@
<script>
// Import Other Supporting Files
import { defineRule } from 'vee-validate';
import { required } from '@/helpers/validation-rules';
import { errorMessages } from '@/constants/error-messages';
import { useMainStore } from "@/store";
import {vinLookupMethodSelections} from '@/constants/vin-lookup-methods';
import { vinLookupMethodSelections } from '@/constants/vin-lookup-methods';
import { settleAllPromises } from "@/helpers/layout-helper";
import globalRules from '@/constants/global-rules';
// Import Component
import ButtonQuestion from '@/digital-components/button-question/button-question.vue';
// Define Validation Rules
defineRule('option-required', required(errorMessages.OPTION_REQUIRED));
export default {
name: 'vin-lookup-methods',
data() {
return {
questionTextFromCms: '',
answersFromCms: [],
rules: {
optionRequired: globalRules.OPTION_REQUIRED
}
};
},
props: {
modelValue: String,
cmsWidgetName: {
type: String,
required,
required: true
},
},

View file

@ -15,7 +15,7 @@
ref="policyNumber"
disableAutoFill
:isDisabled="isPolicyHolderEnabled"
validationRules="policy-number-required" />
:validationRules="rules.policyNumber" />
</div>
</div>
<div class="row mt-4" v-if="this.isCoverageEnabled">
@ -26,7 +26,7 @@
v-model="welcomePageModel.policyZipCode"
isRequired
ref="policyZip"
validationRules="policy-zip-required|policy-zip-format" />
:validationRules="rules.policyZip" />
</div>
</div>
<div class="row mt-4">
@ -41,7 +41,7 @@
disableAutoFill
:max="new Date().toJSON().slice(0,10)"
:min="'1972-12-01'"
validationRules="loss-date-required"
:validationRules="rules.lossDate"
/>
</div>
</div>
@ -59,7 +59,7 @@
inputId="damageCauseQuestionField"
:options="DamageCauseOptions"
disableAutoFill
validationRules="damage-option-required"
:validationRules="rules.damageOption"
placeHolderText="Select an option"
id="welcomeDropdown"
/>
@ -71,7 +71,7 @@
inputId="phoneNumberField"
cmsWidgetName="PhoneNumberQuestion"
v-model="welcomePageModel.phoneNumber"
validationRules="phone-number-required|phone-number-format"
:validationRules="rules.phoneNumber"
isRequired
ref="phoneNumber"
mask="###-###-####"
@ -85,7 +85,7 @@
cmsWidgetName="EmailAddressQuestion"
v-model="welcomePageModel.email"
ref="email"
validationRules="email-address-required|email-address-format"
:validationRules="rules.email"
isRequired
disableAutoFill />
</div>
@ -101,7 +101,7 @@
ref="damageCity"
disableAutoFill
v-if="this.displayDamageCityQuestion"
validationRules="loss-city-required" />
:validationRules="rules.lossCity" />
</div>
</div>
<div class="row">
@ -113,7 +113,7 @@
ref="state"
inputId="8fdf9dc2e13e430eb57529499dceb3eb"
:options="getStates"
validationRules="loss-state-required"
:validationRules="rules.lossState"
isRequired
disableAutoFill
v-if="this.displayDamageStateQuestion"
@ -127,11 +127,11 @@
class="px-0 mt-4"
cmsWidgetName="GlassOnlyQuestion"
v-model="welcomePageModel.isDamageGlassOnly"
inputId = "isDamageGlassOnly"
inputId="isDamageGlassOnly"
:answers="DamageGlassOnlyOptions"
:questionText="DamageGlassOnlyQuestion"
buttonTypeString="listButtonHorizontal"
validationRules="damage-option-required"
:validationRules="rules.damageOption"
isRequired
isSmallQuestionLabelText
ref="glassOnlyDamage"
@ -173,32 +173,17 @@
import BaseFormMixin from '@/mixins/base-form-mixin.js';
import { useMainStore } from '@/store';
import { states } from "@/constants/states"
import globalRules from '@/constants/global-rules';
//define validation rules
defineRule("loss-date-required", required(errorMessages.LOSS_DATE_REQUIRED));
defineRule("loss-cause-required", required(errorMessages.LOSS_CAUSE_REQUIRED));
defineRule("policy-number-required", required(errorMessages.POLICY_NUMBER_REQUIRED));
defineRule("loss-date-required", required(errorMessages.LOSS_DATE_REQUIRED));
defineRule("loss-state-required", required(errorMessages.LOSS_STATE_REQUIRED));
defineRule("loss-city-required", required(errorMessages.LOSS_CITY_REQUIRED));
defineRule("phone-number-required", required(errorMessages.PHONE_NUMBER_FORMAT));
defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
defineRule("damage-option-required", required(errorMessages.DAMAGE_OPTION_REQUIRED));
defineRule("policy-zip-required", required(errorMessages.POLICY_ZIP_REQUIRED));
defineRule("policy-zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.POLICY_ZIP_FORMAT));
defineRule("email-address-format",
regex(
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9_\-.]+)\.([a-zA-Z]{2,})$/,
errorMessages.EMAIL_ADDRESS_FORMAT
));
defineRule("phone-number-format",
regex(
/^(\([0-9]{3}\)|[0-9]{3}) *[-.]? *[0-9]{3} *[-.]? *[0-9]{4}$/,
errorMessages.PHONE_NUMBER_FORMAT
));
export default {
name: "welcome-page",
mixins: [BaseFormMixin],
@ -206,6 +191,16 @@ export default {
return {
welcomePageModel: this.getWelcomePageModelFromStore(),
vehiclesFound: [],
rules: {
policyNumber: 'policy-number-required',
policyZip: 'policy-zip-required|policy-zip-format',
lossDate: 'loss-date-required',
damageOption: 'damage-option-required',
phoneNumber: `${globalRules.PHONE_NUMBER_REQUIRED}|${globalRules.PHONE_NUMBER_FORMAT}`,
email: `${globalRules.EMAIL_ADDRESS_REQUIRED}|${globalRules.EMAIL_ADDRESS_FORMAT}`,
lossCity: 'loss-city-required',
lossState: 'loss-state-required'
}
};
},
setup() {

View file

@ -1,32 +1,32 @@
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import "../node_modules/bootstrap/dist/js/bootstrap.js";
import 'bootstrap/dist/js/bootstrap';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
import { useMainStore } from '@/store';
import baseMixin from "@/mixins/base-mixin.js";
import baseMixin from '@/mixins/base-mixin';
import { createPinia } from 'pinia';
import Maska from "maska";
import LoadScript from "vue-plugin-load-script";
import Maska from 'maska';
import LoadScript from 'vue-plugin-load-script';
import analyticsMixin from "@/mixins/analytics-mixin.js";
import experimentMixin from "@/mixins/experiment-mixin.js";
import analyticsMixin from '@/mixins/analytics-mixin';
import experimentMixin from '@/mixins/experiment-mixin';
import defineGlobalRules from '@/helpers/global-rule-definer';
import router from './router';
import App from './App.vue';
// Vue App Setup
const vueApp = createApp(App);
/**
/**
* Needed to make injections reactively linked to the provider.
* This is not needed once Vue.js is in version 3.3
* https://vuejs.org/guide/components/provide-inject.html#working-with-reactivity
*/
vueApp.config.unwrapInjectedRef = true;
vueApp.config.compilerOptions.isCustomElement = (tag) => {
return (tag === 'siteSubHeader' ||
tag === 'ServicePackages' ||
tag === 'servicePackageQuestion');
}
vueApp.config.compilerOptions.isCustomElement = (tag) =>
(tag === 'siteSubHeader'
|| tag === 'ServicePackages'
|| tag === 'servicePackageQuestion');
// Pinia
const pinia = createPinia();
@ -34,7 +34,6 @@ vueApp.use(pinia);
pinia.use(piniaPluginPersistedstate);
useMainStore().populateInitialState();
// Additional Vue items to setup
vueApp.use(router);
vueApp.use(Maska);
@ -43,4 +42,7 @@ vueApp.mixin(baseMixin);
vueApp.mixin(analyticsMixin);
vueApp.mixin(experimentMixin);
vueApp.mount("#app");
vueApp.mount('#app');
// define global rules
defineGlobalRules();

View file

@ -23,6 +23,10 @@ html {
}
}
&.list-card {
border: 1px solid $red;
}
&.ui-radio {
&:hover {
input[type="radio"],