Merge branch 'develop' into feature/kiener/INSR-7753

This commit is contained in:
Katie Kroell 2026-01-22 15:36:53 -05:00
commit d0b970e68a
61 changed files with 2359 additions and 1615 deletions

199
package-lock.json generated
View file

@ -142,7 +142,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.0.tgz",
"integrity": "sha512-reM4+U7B9ss148rh2n1Qs9ASS+w94irYXga7c2jaQv9RVzpS7Mv1a9rnYYwuDa45G+DkORt9g6An2k/V4d9LbQ==",
"dev": true,
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.1.0",
"@babel/code-frame": "^7.18.6",
@ -2374,7 +2373,8 @@
"integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==",
"dev": true,
"license": "MIT",
"optional": true
"optional": true,
"peer": true
},
"node_modules/@hapi/hoek": {
"version": "9.3.0",
@ -3281,6 +3281,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"@gar/promisify": "^1.0.1",
"semver": "^7.3.5"
@ -3293,6 +3294,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"semver": "bin/semver.js"
},
@ -3308,6 +3310,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"mkdirp": "^1.0.4",
"rimraf": "^3.0.2"
@ -3323,6 +3326,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"bin": {
"mkdirp": "bin/cmd.js"
},
@ -3393,7 +3397,6 @@
"integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"playwright": "1.56.1"
},
@ -3973,7 +3976,6 @@
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.19.1.tgz",
"integrity": "sha512-P6iIPyYQ+qH8CvGauAqanhVnjrnRe0IZFSYCeGkSRW9q3u8bdVn2NPI+lasFyVsEQn1J/IFmp5Aax41+dAP9wg==",
"dev": true,
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
@ -4440,7 +4442,6 @@
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz",
"integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==",
"dev": true,
"peer": true,
"dependencies": {
"@types/linkify-it": "*",
"@types/mdurl": "*"
@ -4625,7 +4626,6 @@
"integrity": "sha512-h2lUByouOXFAlMec2mILeELUbME5SZRN/7R9Cw2RD2lRQQY08MWMM+PmVVKKJNK1aIwqTo9t/0CvOxwPbRIE2Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.23.0",
"@typescript-eslint/types": "8.23.0",
@ -5419,7 +5419,6 @@
"resolved": "https://registry.npmjs.org/@vue/cli-service/-/cli-service-5.0.8.tgz",
"integrity": "sha512-nV7tYQLe7YsTtzFrfOMIHc5N2hp5lHG2rpYr0aNja9rNljdgcPZLyQRb2YRivTHqTv7lI962UXFURcpStHgyFw==",
"dev": true,
"peer": true,
"dependencies": {
"@babel/helper-compilation-targets": "^7.12.16",
"@soda/friendly-errors-webpack-plugin": "^1.8.0",
@ -5640,7 +5639,6 @@
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz",
"integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==",
"peer": true,
"dependencies": {
"@babel/parser": "^7.20.15",
"@vue/compiler-core": "3.3.4",
@ -5775,7 +5773,6 @@
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.4.tgz",
"integrity": "sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==",
"peer": true,
"dependencies": {
"@vue/compiler-ssr": "3.3.4",
"@vue/shared": "3.3.4"
@ -5848,7 +5845,6 @@
"resolved": "https://registry.npmjs.org/@vue/vue3-jest/-/vue3-jest-27.0.0.tgz",
"integrity": "sha512-VL61CgZBoQqayXfzlZJHHpZuX4lsT8dmdZMJzADhdAJjKu26JBpypHr/2ppevxItljPiuALQW4MKhhCXZRXnLg==",
"dev": true,
"peer": true,
"dependencies": {
"@babel/plugin-transform-modules-commonjs": "^7.2.0",
"chalk": "^2.1.0",
@ -6097,7 +6093,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz",
"integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==",
"dev": true,
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@ -6201,6 +6196,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"humanize-ms": "^1.2.1"
},
@ -6215,6 +6211,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"clean-stack": "^2.0.0",
"indent-string": "^4.0.0"
@ -6228,7 +6225,6 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@ -6361,7 +6357,8 @@
"integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
"dev": true,
"license": "ISC",
"optional": true
"optional": true,
"peer": true
},
"node_modules/arch": {
"version": "2.2.0",
@ -6400,6 +6397,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"delegates": "^1.0.0",
"readable-stream": "^3.6.0"
@ -6599,7 +6597,6 @@
"integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
@ -6663,7 +6660,6 @@
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz",
"integrity": "sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==",
"dev": true,
"peer": true,
"dependencies": {
"@jest/transform": "^27.5.1",
"@jest/types": "^27.5.1",
@ -6984,6 +6980,7 @@
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"file-uri-to-path": "1.0.0"
}
@ -7128,7 +7125,6 @@
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001646",
"electron-to-chromium": "^1.5.4",
@ -7217,6 +7213,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"@npmcli/fs": "^1.0.0",
"@npmcli/move-file": "^1.0.1",
@ -7248,6 +7245,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"bin": {
"mkdirp": "bin/cmd.js"
},
@ -7490,6 +7488,7 @@
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
"dev": true,
"license": "ISC",
"peer": true,
"engines": {
"node": ">=10"
}
@ -7534,6 +7533,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=6"
}
@ -7748,6 +7748,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"color-support": "bin.js"
}
@ -8090,7 +8091,8 @@
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
"dev": true,
"license": "ISC",
"optional": true
"optional": true,
"peer": true
},
"node_modules/consolidate": {
"version": "0.15.1",
@ -8292,7 +8294,6 @@
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.1.tgz",
"integrity": "sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw==",
"dev": true,
"peer": true,
"dependencies": {
"icss-utils": "^5.1.0",
"postcss": "^8.4.7",
@ -8369,7 +8370,6 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"dev": true,
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
@ -8642,6 +8642,7 @@
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"mimic-response": "^3.1.0"
},
@ -8706,6 +8707,7 @@
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=4.0.0"
}
@ -8922,7 +8924,8 @@
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
"dev": true,
"license": "MIT",
"optional": true
"optional": true,
"peer": true
},
"node_modules/depd": {
"version": "2.0.0",
@ -8949,6 +8952,7 @@
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8"
}
@ -8969,10 +8973,11 @@
"dev": true
},
"node_modules/diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
"integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
}
@ -9125,7 +9130,6 @@
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
"dev": true,
"peer": true,
"engines": {
"node": ">=10"
}
@ -9297,6 +9301,33 @@
"node": ">= 0.8"
}
},
"node_modules/encoding": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"iconv-lite": "^0.6.2"
}
},
"node_modules/encoding/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
@ -9335,6 +9366,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=6"
}
@ -9345,7 +9377,8 @@
"integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
"dev": true,
"license": "MIT",
"optional": true
"optional": true,
"peer": true
},
"node_modules/error-ex": {
"version": "1.3.2",
@ -9604,7 +9637,6 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@ -9736,7 +9768,6 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz",
"integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==",
"dev": true,
"peer": true,
"dependencies": {
"array-includes": "^3.1.4",
"array.prototype.flat": "^1.2.5",
@ -10363,6 +10394,7 @@
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"dev": true,
"license": "(MIT OR WTFPL)",
"peer": true,
"engines": {
"node": ">=6"
}
@ -10621,7 +10653,8 @@
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/fill-range": {
"version": "7.1.1",
@ -10886,7 +10919,8 @@
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/fs-extra": {
"version": "9.1.0",
@ -10909,6 +10943,7 @@
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
"dev": true,
"license": "ISC",
"peer": true,
"dependencies": {
"minipass": "^3.0.0"
},
@ -10986,6 +11021,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"aproba": "^1.0.3 || ^2.0.0",
"color-support": "^1.1.3",
@ -11099,7 +11135,8 @@
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/glob": {
"version": "7.2.3",
@ -11291,7 +11328,8 @@
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
"dev": true,
"license": "ISC",
"optional": true
"optional": true,
"peer": true
},
"node_modules/hash-sum": {
"version": "2.0.0",
@ -11476,7 +11514,8 @@
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
"dev": true,
"license": "BSD-2-Clause",
"optional": true
"optional": true,
"peer": true
},
"node_modules/http-deceiver": {
"version": "1.2.7",
@ -11587,6 +11626,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"ms": "^2.0.0"
}
@ -11731,7 +11771,8 @@
"integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==",
"dev": true,
"license": "ISC",
"optional": true
"optional": true,
"peer": true
},
"node_modules/inflight": {
"version": "1.0.6",
@ -11776,6 +11817,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"jsbn": "1.1.0",
"sprintf-js": "^1.1.3"
@ -11790,7 +11832,8 @@
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
"dev": true,
"license": "BSD-3-Clause",
"optional": true
"optional": true,
"peer": true
},
"node_modules/ipaddr.js": {
"version": "2.0.1",
@ -12044,7 +12087,8 @@
"integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==",
"dev": true,
"license": "MIT",
"optional": true
"optional": true,
"peer": true
},
"node_modules/is-map": {
"version": "2.0.2",
@ -12473,7 +12517,6 @@
"resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz",
"integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==",
"dev": true,
"peer": true,
"dependencies": {
"@jest/core": "^27.5.1",
"import-local": "^3.0.2",
@ -15187,7 +15230,8 @@
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
"dev": true,
"license": "MIT",
"optional": true
"optional": true,
"peer": true
},
"node_modules/jsdoc": {
"version": "4.0.2",
@ -15513,10 +15557,11 @@
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
@ -15860,6 +15905,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"agentkeepalive": "^4.1.3",
"cacache": "^15.2.0",
@ -15889,6 +15935,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 6"
}
@ -15900,6 +15947,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@tootallnate/once": "1",
"agent-base": "6",
@ -16122,6 +16170,7 @@
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@ -16162,7 +16211,6 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"dev": true,
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
@ -16258,6 +16306,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"minipass": "^3.0.0"
},
@ -16272,6 +16321,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"minipass": "^3.1.0",
"minipass-sized": "^1.0.3",
@ -16291,6 +16341,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"minipass": "^3.0.0"
},
@ -16305,6 +16356,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"minipass": "^3.0.0"
},
@ -16319,6 +16371,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"minipass": "^3.0.0"
},
@ -16332,6 +16385,7 @@
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"minipass": "^3.0.0",
"yallist": "^4.0.0"
@ -16357,7 +16411,8 @@
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/module-alias": {
"version": "2.2.2",
@ -16428,7 +16483,8 @@
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/natural-compare": {
"version": "1.4.0",
@ -16473,6 +16529,7 @@
"integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"semver": "^7.3.5"
},
@ -16486,6 +16543,7 @@
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"dev": true,
"license": "ISC",
"peer": true,
"bin": {
"semver": "bin/semver.js"
},
@ -16498,7 +16556,8 @@
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/node-fetch": {
"version": "2.6.7",
@ -16559,6 +16618,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"env-paths": "^2.2.0",
"glob": "^7.1.4",
@ -16585,6 +16645,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"abbrev": "1"
},
@ -16602,6 +16663,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"semver": "bin/semver.js"
},
@ -16616,6 +16678,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"isexe": "^2.0.0"
},
@ -16724,6 +16787,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"are-we-there-yet": "^3.0.0",
"console-control-strings": "^1.1.0",
@ -17077,6 +17141,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"aggregate-error": "^3.0.0"
},
@ -17313,7 +17378,6 @@
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.4.tgz",
"integrity": "sha512-vYlnDu+Y/FXxv1ABo1vhjC+IbqvzUdiUC3sfDRrRyY2CQSrqqaa+iiHmqtARFxJVqWQMCJfXx1PBvFs9aJVLXQ==",
"peer": true,
"dependencies": {
"@vue/devtools-api": "^6.5.0",
"vue-demi": ">=0.14.5"
@ -17468,7 +17532,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@ -18001,6 +18064,7 @@
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
@ -18116,7 +18180,8 @@
"integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==",
"dev": true,
"license": "ISC",
"optional": true
"optional": true,
"peer": true
},
"node_modules/promise-retry": {
"version": "2.0.1",
@ -18125,6 +18190,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"err-code": "^2.0.2",
"retry": "^0.12.0"
@ -18140,6 +18206,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 4"
}
@ -18319,6 +18386,7 @@
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"dev": true,
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
"peer": true,
"dependencies": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
@ -18335,6 +18403,7 @@
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@ -18783,7 +18852,6 @@
"resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz",
"integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==",
"dev": true,
"peer": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
@ -19064,7 +19132,8 @@
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"dev": true,
"license": "ISC",
"optional": true
"optional": true,
"peer": true
},
"node_modules/set-function-length": {
"version": "1.2.2",
@ -19252,7 +19321,8 @@
"url": "https://feross.org/support"
}
],
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/simple-get": {
"version": "4.0.1",
@ -19274,6 +19344,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
@ -19316,6 +19387,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
@ -19348,6 +19420,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"ip-address": "^9.0.5",
"smart-buffer": "^4.2.0"
@ -19364,6 +19437,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"agent-base": "^6.0.2",
"debug": "^4.3.3",
@ -19912,6 +19986,7 @@
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
"dev": true,
"license": "ISC",
"peer": true,
"dependencies": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
@ -19930,6 +20005,7 @@
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
@ -19942,7 +20018,8 @@
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"dev": true,
"license": "ISC"
"license": "ISC",
"peer": true
},
"node_modules/tar-fs/node_modules/tar-stream": {
"version": "2.2.0",
@ -19950,6 +20027,7 @@
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
@ -19979,6 +20057,7 @@
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
"dev": true,
"license": "ISC",
"peer": true,
"engines": {
"node": ">=8"
}
@ -19989,6 +20068,7 @@
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"mkdirp": "bin/cmd.js"
},
@ -20225,7 +20305,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@ -20503,6 +20582,7 @@
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"safe-buffer": "^5.0.1"
},
@ -20558,7 +20638,6 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"devOptional": true,
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -20698,6 +20777,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"unique-slug": "^2.0.0"
}
@ -20709,6 +20789,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"imurmurhash": "^0.1.4"
}
@ -20872,7 +20953,6 @@
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@ -21004,7 +21084,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@ -21018,7 +21097,6 @@
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4",
@ -21171,7 +21249,6 @@
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz",
"integrity": "sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.3.4",
"@vue/compiler-sfc": "3.3.4",
@ -21482,7 +21559,6 @@
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz",
"integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==",
"dev": true,
"peer": true,
"dependencies": {
"@types/estree": "^1.0.5",
"@webassemblyjs/ast": "^1.12.1",
@ -21698,7 +21774,6 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"dev": true,
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
@ -21807,7 +21882,6 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"dev": true,
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
@ -21874,7 +21948,6 @@
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
"integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
"dev": true,
"peer": true,
"engines": {
"node": ">=10.13.0"
}
@ -22047,6 +22120,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"string-width": "^1.0.2 || 2 || 3 || 4"
}
@ -22182,7 +22256,6 @@
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10.0.0"
},

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="21" viewBox="0 0 20 21">
<path fill="#E86421" fill-rule="nonzero" d="M10 .5c5.523 0 10 4.477 10 10s-4.477 10-10 10-10-4.477-10-10S4.477.5 10 .5zm.043 13.6a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm.77-9.2h-1.54l-.088.009a.502.502 0 0 0-.299.193.66.66 0 0 0-.125.47l.747 6.806.017.096c.065.249.263.425.495.426l.084-.008c.22-.042.396-.246.427-.51l.793-6.805.004-.102a.651.651 0 0 0-.127-.37.489.489 0 0 0-.388-.205z"/>
</svg>

After

Width:  |  Height:  |  Size: 474 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="9" viewBox="0 0 15 9">
<path fill="#E86421" fill-rule="nonzero" d="M7.5 0a.806.806 0 0 0-.593.265L.246 7.455a.957.957 0 0 0 0 1.28.796.796 0 0 0 1.185 0l6.07-6.55 6.068 6.55a.796.796 0 0 0 1.186 0 .957.957 0 0 0 0-1.28L8.093.265A.806.806 0 0 0 7.5 0"/>
</svg>

After

Width:  |  Height:  |  Size: 323 B

View file

@ -1,9 +0,0 @@
const buttonVariants = Object.freeze({
primary: 'primary',
secondary: 'secondary',
success: 'success',
link: 'link',
navigation: 'navigation'
});
export default buttonVariants;

View file

@ -0,0 +1,15 @@
export const buttonVariants = Object.freeze({
primary: 'primary',
secondary: 'secondary',
success: 'success',
link: 'link',
navigation: 'navigation'
});
export const dropdownVariants = Object.freeze({
primary: 'primary',
compact: 'compact'
});
export const modalPositions = Object.freeze({
center: 'center',
edge: 'edge'
});

View file

@ -23,8 +23,13 @@ const errorMessages = Object.freeze({
LAST_NAME_REQUIRED: 'Please enter your last name',
EMAIL_ADDRESS_REQUIRED: 'Email is required.',
EMAIL_ADDRESS_FORMAT: 'Please enter a valid email address.',
SERVICE_ZIP_REQUIRED: 'Please enter your service ZIP',
SERVICE_ZIP_FORMAT: 'Please enter a valid service ZIP',
SERVICE_ZIP_REQUIRED: 'ZIP code is required',
SERVICE_ZIP_FORMAT: 'Invalid ZIP, please enter a new ZIP (5 characters)',
MOBILE_SERVICE_ZIP_FORMAT: 'Invalid ZIP code',
NO_SERVICE_IN_AREA: (area) => `We're sorry, but we don't offer service in ${area} at this time.`,
PROVIDER_REQUIRED: 'Please select a provider.',
INVALID_ZIP: 'The ZIP you entered was invalid. Please enter a valid ZIP.',
ZIP_CODE_NOT_SERVICED_FOR_VEHICLE: 'We do not currently offer glass service for your vehicle in this ZIP code. Please try another ZIP code.',
VIN_REQUIRED: 'Please enter your VIN',
VIN_FORMAT:
// eslint-disable-next-line max-len

View file

@ -71,6 +71,9 @@ export const pageProgressMapper = {
'tpa-search': {
percent: 85
},
'tpa-submit': {
percent: 95
},
'tpa-confirmation': {
percent: 100
},

View file

@ -294,6 +294,11 @@ export default {
this.resetField({
value: newValue
});
},
answers() {
this.resetField({
value: this.modelValue
});
}
},
beforeMount() {

View file

@ -2,8 +2,9 @@
<div
class="dropdown-question"
:class="{
'has-error' : (meta.touched && errors && errors.length && !isDisabled) || hasError,
'has-success': (meta.touched && (!errors || !errors.length) && !isDisabled)
'has-error' : (meta?.touched && errors && errors.length && !isDisabled) || hasError,
'has-success': (meta?.touched && (!errors || !errors.length) && !isDisabled),
[`dropdown-${variant}`]: true
}">
<label
:for="inputId"
@ -34,7 +35,7 @@
</option>
</select>
<div
v-show="meta.touched && errorMessage && !isDisabled"
v-show="meta?.touched && errorMessage && !isDisabled"
class="row mt-1 form-test-error">
<span role="alert">{{ errorMessage }}</span>
</div>
@ -42,6 +43,7 @@
</template>
<script>
import { dropdownVariants } from '@/constants/component-variants';
import { useField } from 'vee-validate';
export default {
@ -59,7 +61,11 @@ export default {
validationRules: String,
cmsWidgetName: String,
hasError: Boolean,
placeHolderText: String
placeHolderText: String,
variant: {
type: String,
default: dropdownVariants.primary
}
},
emits: ['update:modelValue'],
setup(props) {
@ -154,5 +160,30 @@ export default {
filter: grayscale(100%);
}
}
&.dropdown-compact {
display: flex;
align-items: center;
justify-content: flex-end;
margin-bottom: 1.25rem;
.form-label {
color: $darker-gray;
font-weight: $font-weight-light;
margin: 0;
}
.form-select {
color: $heritage-blue-secondary;
background-position-x: 98%;
padding: 0;
padding-left: .3125rem;
width: 5.625rem;
min-height: auto;
border: none;
box-shadow: none;
&:focus,
&:focus-visible {
border: .125rem solid $black;
}
}
}
}
</style>

View file

@ -13,19 +13,19 @@
}">
<div class="modal-dialog"
:class="{
'modal-dialog-edge': modalPosition === 'edge',
'modal-dialog-centered': modalPosition === 'center'
'modal-dialog-edge': modalPosition === modalPositions.edge,
'modal-dialog-centered': modalPosition === modalPositions.center
}">
<div
class="modal-content"
role="dialog"
aria-modal="true">
<div class="modal-header mb-2 mt-2">
<label
<span
v-if="headerText"
class="modal-title d-flex justify-content-center pb-0 w-100">
{{ headerText }}
</label>
class="modal-title d-flex justify-content-center pb-0 w-100"
:innerHTML="headerText">
</span>
<button
ref="closeButton"
type="button"
@ -60,6 +60,7 @@
import ButtonMain from '@/ux-components/button-main/button-main.vue';
import { Modal } from 'bootstrap';
import { useForm } from 'vee-validate';
import { modalPositions } from '@/constants/component-variants';
export default {
// eslint-disable-next-line vue/multi-word-component-names
@ -84,7 +85,11 @@ export default {
},
modalPosition: {
type: String,
default: 'edge'
default: modalPositions.edge
},
allowInvalidSubmit: {
type: Boolean,
default: false
}
},
emits: ['footer-button-event', 'isModalOpened'],
@ -102,7 +107,8 @@ export default {
modalId,
meta,
validate,
resetForm
resetForm,
modalPositions
};
},
computed: {
@ -121,7 +127,7 @@ export default {
methods: {
async validateAndEmit() {
const validationResult = await this.validate();
if (validationResult.valid) {
if (validationResult.valid || this.allowInvalidSubmit) {
this.$emit('footer-button-event');
}
},
@ -257,6 +263,9 @@ export default {
border-radius: 1.5rem;
overflow: auto;
}
.modal-header {
justify-content: center;
}
}
}
&.show .modal-dialog {

View file

@ -51,7 +51,7 @@ export default {
props: {
customText: String, // used to allow the insert of token values into textblock
justifyText: String, // left, right, center
typeStyle: String, // h1-h6, body, small, label, caption
typeStyle: String, // h1-h6, body, small, label, caption, disclaimer
// (see Figma or Confluence documentation)
fontWeight: String, // bold=500, default is 400
cmsWidgetName: String,
@ -109,5 +109,12 @@ export default {
&.dark {
color: $black;
}
&.disclaimer {
color: $lighter-gray;
font-weight: $font-weight-light;
font-size: .8125rem;
font-style: italic;
margin-top: .625rem;
}
}
</style>

View file

@ -16,7 +16,8 @@
class="input-wrapper"
:class="[
includeSearchIcon ? 'has-search-icon' : '',
includeSelectIcon ? 'has-select-icon' : ''
includeSelectIcon ? 'has-select-icon' : '',
hasTextButton ? 'has-text-button' : ''
]">
<input
:id="inputId"
@ -61,6 +62,13 @@
data-bs-toggle="modal"
:data-bs-target="'#' + cmsWidgetName"
aria-label="Select button" />
<buttonMain
v-if="hasTextButton"
ref="buttonMain"
variant="primary"
:buttonText="buttonText"
@clickEvent="clickedTextButton">
</buttonMain>
</div>
<div
v-show="errorMessage"
@ -76,9 +84,13 @@
<script>
import { useField, validate } from 'vee-validate';
import { extractSpanText } from '@/helpers/text-helper';
import buttonMain from '@/ux-components/button-main/button-main.vue';
export default {
name: 'textbox-question',
components: {
buttonMain
},
props: {
type: {
type: String,
@ -118,7 +130,11 @@ export default {
includeSelectIcon: Boolean,
min: String,
max: String,
disableAutoFill: Boolean
disableAutoFill: Boolean,
buttonCallback: {
type: Function,
default: () => {}
}
},
emits: ['focus', 'update:modelValue', 'textboxQuestionEvent.inputIdAssigned', 'click-event'],
setup(props) {
@ -161,6 +177,12 @@ export default {
questionTextForAria() {
return extractSpanText(this.getCmsContent(this.cmsWidgetName, 'QuestionText'));
},
buttonText() {
return this.getCmsContent(this.cmsWidgetName, 'ButtonText');
},
hasTextButton() {
return this.buttonText && this.buttonText.length > 0;
},
value: {
get() {
return this.modelValue;
@ -227,6 +249,13 @@ export default {
if (this.meta.valid) {
this.$emit('click-event');
}
},
async clickedTextButton() {
const result = await validate(this.value, this.validationRules);
if (result.valid) {
this.handleChange(this.value);
this.$emit('click-event');
}
}
}
};
@ -273,7 +302,7 @@ input::-webkit-date-and-time-value {
color: #4d5151;
}
.form-test-error span {
color: #d4281c;
color: $red;
font-size: 1rem;
font-weight: 400;
}
@ -318,6 +347,19 @@ input::-webkit-date-and-time-value {
display: flex;
}
}
&.has-text-button {
display: flex;
input[type='text'] {
border-radius: 1.5rem;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
button {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
min-width: 6.875rem;
}
}
}
input {
&.has-icon {
@ -338,8 +380,8 @@ input::-webkit-date-and-time-value {
.form-control {
border: $border-input;
border-radius: $border-radius;
min-height: 3rem;
max-height: 3rem;
min-height: 2.8125rem;
max-height: 2.8125rem;
padding: 0.75rem 1rem;
letter-spacing: inherit;
box-shadow: $box-shadow-input;

View file

@ -5,6 +5,7 @@ function processZipCodeResults(request) {
containsMilitaryBase: serviceZipValidationResponse.containsMilitaryBase,
isValid: serviceZipValidationResponse.isValid,
isServiceable: serviceZipValidationResponse.isServiceable,
city: serviceZipValidationResponse.city,
state: serviceZipValidationResponse.state,
zipCodeCtu: serviceZipValidationResponse.zipCodeCtu
})).catch(() => ({

View file

@ -45,11 +45,8 @@ export function createUnorderedListFromStringOfParagraphs(stringOfParagraphs) {
* @returns {string}
*/
export function toTitleCase(text) {
const temp = text?.toLowerCase()?.split(' ') ?? [];
for (let i = 0; i < temp.length; i++) {
temp[i] = temp[i].charAt(0).toUpperCase() + temp[i].slice(1);
}
return temp.join(' ');
let temp = text?.toLowerCase() ?? '';
return temp.replace(/(^|\s|-)\S/g, (letter) => letter.toUpperCase());
}
/**

View file

@ -314,7 +314,6 @@ describe('Google Map', () => {
// Arrange
const wrapper = shallowMount(googleMap, {});
await awaitingSetupTicks(wrapper);
const numberOfExtendCallsDuringMount = 2;
mockGeocode.mockImplementation(() => {
const result = {
@ -329,7 +328,7 @@ describe('Google Map', () => {
const result = wrapper.vm.getBounds(locations);
// Assert
expect(mockExtend).toHaveBeenCalledTimes(numberOfExtendCallsDuringMount + numberOfExtendCallsPostMount);
expect(mockExtend).toHaveBeenCalledTimes(numberOfExtendCallsPostMount);
expect(result).toBe(mockLatLngBound);
}
);

View file

@ -55,7 +55,7 @@ export default {
const { AdvancedMarkerElement, PinElement } = await window.google.maps.importLibrary('marker');
markers?.forEach((marker, index) => {
const pinElement = new PinElement({
glyph: String.fromCharCode('A'.charCodeAt(0) + index),
glyphText: String.fromCharCode('A'.charCodeAt(0) + index),
glyphColor: '#000000',
borderColor: '#000000'
});
@ -107,9 +107,11 @@ export default {
async getBoundsFromAddress(address) {
await this.setGeocoder();
const result = await this.geocoder.geocode({ address });
return result?.results?.length > 0
? result.results[0].geometry?.bounds
: null;
if (result?.results?.length > 0) {
return result.results[0].geometry?.bounds ?? null;
} else {
return null;
}
},
async createMapWithMarkersForAddresses(markers) {
const markerPositions = await this.getLocationsFromAddresses(markers);
@ -117,9 +119,12 @@ export default {
const map = await this.getMap(zipBounds?.getCenter());
await this.addMarkersToMap(map, markerPositions);
let positionsToDisplay = markerPositions.map((marker) => marker.position);
if(zipBounds) {
positionsToDisplay.push(zipBounds.getNorthEast());
positionsToDisplay.push(zipBounds.getSouthWest());
}
const positionsToDisplay = markerPositions.map((marker) => marker.position)
.concat(zipBounds?.getNorthEast(), zipBounds?.getSouthWest());
const bounds = this.getBounds(positionsToDisplay);
map.fitBounds(bounds);
}

View file

@ -2,13 +2,13 @@
exports[`Shop list button should render correctly with all relevant props 1`] = `
<transition-stub name="fade" mode="out-in" appear="false" persisted="false" css="true" selectedvalue="selected value">
<base-input-button-stub modelvalue="value of modal" groupname="name of group" buttonwrapperclasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 mb-2" ismultiselect="false" validationrules="" isrequired="true" selectinginitiatesload="false" suppresserror="false" buttonlabel="Label of button" buttonlabelsubcopy="Sub copy of button" buttonbodycopy="button body copy" screenreaderonlytext="screen reader only text" additionalbuttondata="[object Object]" alttext="" iswide="false" value="1234"></base-input-button-stub>
<base-input-button-stub modelvalue="value of modal" groupname="name of group" buttonwrapperclasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 no-hover mb-2" ismultiselect="false" validationrules="" isrequired="true" selectinginitiatesload="false" suppresserror="false" buttonlabel="Label of button" buttonlabelsubcopy="Sub copy of button" buttonbodycopy="button body copy" screenreaderonlytext="screen reader only text" additionalbuttondata="[object Object]" alttext="" iswide="false" value="1234"></base-input-button-stub>
</transition-stub>
`;
exports[`Shop list button should render correctly with required props 1`] = `
<transition-stub name="fade" mode="out-in" appear="false" persisted="false" css="true">
<base-input-button-stub modelvalue="value of modal" groupname="name of group" buttonwrapperclasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 mb-2" ismultiselect="false" validationrules="" isrequired="true" selectinginitiatesload="false" suppresserror="false" alttext="" iswide="false" value="1234"></base-input-button-stub>
<base-input-button-stub modelvalue="value of modal" groupname="name of group" buttonwrapperclasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 no-hover mb-2" ismultiselect="false" validationrules="" isrequired="true" selectinginitiatesload="false" suppresserror="false" alttext="" iswide="false" value="1234"></base-input-button-stub>
</transition-stub>
`;

View file

@ -149,10 +149,9 @@ describe('Shop list button', () => {
// Assert
expect(buttonLabelSubCopy.exists()).toBeTruthy();
expect(buttonLabelSubCopy.classes().length).toBe(4);
expect(buttonLabelSubCopy.classes().length).toBe(3);
expect(buttonLabelSubCopy.classes()).toContain('m-0');
expect(buttonLabelSubCopy.classes()).toContain('caption');
expect(buttonLabelSubCopy.classes()).toContain('ms-2');
expect(buttonLabelSubCopy.classes()).toContain('button-label-sub-copy');
expect(buttonLabelSubCopy.classes()).toContain(textPosition);
});
test('when "textPosition" prop not provided', async () => {
@ -173,10 +172,9 @@ describe('Shop list button', () => {
// Assert
expect(buttonLabelSubCopy.exists()).toBeTruthy();
expect(buttonLabelSubCopy.classes().length).toBe(3);
expect(buttonLabelSubCopy.classes().length).toBe(2);
expect(buttonLabelSubCopy.classes()).toContain('m-0');
expect(buttonLabelSubCopy.classes()).toContain('caption');
expect(buttonLabelSubCopy.classes()).toContain('ms-2');
expect(buttonLabelSubCopy.classes()).toContain('button-label-sub-copy');
});
});
describe('availability indicator block with expected when displayAvailabilityIndicators true', () => {
@ -378,8 +376,7 @@ describe('Shop list button', () => {
// Assert
expect(buttonBodyCopy.exists()).toBeTruthy();
expect(buttonBodyCopy.classes()).toContain('m-0');
expect(buttonBodyCopy.classes()).toContain('button-label-sub-copy');
expect(buttonBodyCopy.classes()).toContain('small');
expect(buttonBodyCopy.classes()).toContain('button-body-copy');
});
test('screen reader only text when screenReaderOnlyText provided', async () => {
// Arrange

View file

@ -5,35 +5,39 @@
<baseInputButton
v-bind="$props"
v-model="selectedValue"
buttonWrapperClasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 mb-2">
buttonWrapperClasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 no-hover mb-2">
<div
:aria-label="buttonLabel"
class="button-content list-button-content d-flex flex-column justify-content-center py-3 px-4">
<div class="row-one">
<div class="availability-indicator-spacer"></div>
<span
id="buttonLabelSpan"
class="m-0 button-label-copy"
:class="textPosition">{{ buttonLabel }}
</span>
<span class="separator"></span>
<span
id="buttonLabelSubCopySpan"
class="m-0 caption ms-2"
class="m-0 button-label-sub-copy"
:class="textPosition">{{ buttonLabelSubCopy }}
</span>
<div
v-if="displayAvailabilityIndicators"
id="availabilityIndicatorBlock"
class="availability-indicator rounded-pill"
:class="availabilityRatingClass">
<div class="availability-indicator-container">
<div
id="availabilityIndicator"
class="d-flex align-items-center">
v-if="displayAvailabilityIndicators"
id="availabilityIndicatorBlock"
class="availability-indicator rounded-pill"
:class="availabilityRatingClass">
<div
id="availabilityBadge"
class="availability-badge"
:class="availabilityRating == 'high' ? 'green' : 'orange'">
id="availabilityIndicator"
class="d-flex align-items-center">
<div
id="availabilityBadge"
class="availability-badge"
:class="availabilityRatingClass">
</div>
<span class="m-0 button-auxillary-copy">{{ badgeText }}</span>
</div>
<span class="m-0 button-auxillary-copy">{{ badgeText }}</span>
</div>
</div>
</div>
@ -41,7 +45,7 @@
<span
v-if="buttonBodyCopy"
id="buttonBodyCopy"
class="m-0 button-label-sub-copy small"
class="m-0 button-body-copy"
v-html="buttonBodyCopy"></span>
</div>
<span
@ -103,116 +107,128 @@ export default {
<style lang="scss" scoped>
@import "@/styles/ux-variables-svg-strings.scss";
$heritage-border-radius: 60px;
$heritage-border-color: #CACBCC;
$heritage-box-shadow: 0 1px 5px rgba($black, 0.2);
$heritage-checked-background-color: #e7f1f6;
$heritage-checked-border-color: #0070d1;
.list-button {
outline: none;
input[type="radio"],
input[type="checkbox"] {
position: static; //override bootstrap
&:focus-visible + .list-button-content {
box-shadow: 0 0 0 2.5px $blue;
}
&:focus + .list-button-content {
box-shadow: 0 0 0 2.5px $blue;
}
&:checked + .list-button-content {
color: $black;
font-weight: 500;
background: $blue-100;
box-shadow: 0 0 0 1px $blue;
}
&:checked:focus + .list-button-content {
box-shadow: 0 0 0 2.5px $blue;
}
&:checked + .list-button-content p,
&:checked + .list-button-content span {
font-weight: 500;
}
}
outline: none;
input[type="radio"],
input[type="checkbox"] {
position: static; //override bootstrap
&:checked + .list-button-content {
background: $heritage-checked-background-color;
border-color: $heritage-checked-border-color;
box-shadow: 0 0 0 1px $blue;
.button-label-copy {
font-weight: 500;
color: $black;
}
}
&:focus:checked + .list-button-content {
border-width: 2.5px;
}
}
}
.list-button-content {
color: $gray-600;
position: relative;
background: $white;
transition: all 150ms linear;
border-radius: $border-radius-lg;
border: 1px solid $gray-500;
width: 100%;
outline: none;
span {
&.small {
font-size: 0.75rem;
color: $gray-550;
}
}
color: $darker-gray;
font-weight: $font-weight-normal;
position: relative;
background: $white;
transition: all 150ms linear;
border-radius: $heritage-border-radius;
border: 1px solid $heritage-border-color;
box-shadow: $heritage-box-shadow;
width: 100%;
outline: none;
}
.button-content {
row-gap: 0.25rem;
row-gap: 0.25rem;
.row-one {
display: flex;
align-items: center;
line-height: 1.5rem;
.row-one {
display: flex;
align-items: center;
justify-content: center;
line-height: 1.5rem;
.button-label-copy {
font-weight: 500;
}
.separator {
height: 1.25rem;
margin: 0rem .3125rem;
border-left: 1px solid $heritage-border-color;
}
.availability-indicator {
background-repeat: no-repeat;
display: flex;
align-items: center;
margin-left: auto;
padding: 0.125rem 0.5rem;
.availability-indicator-spacer {
width: 8rem;
margin-right: auto;
}
.availability-badge {
display: inline-flex;
width: 13px;
height: 12px;
background-position: center;
background-repeat: no-repeat;
margin: 0 0.25rem 0 0;
.availability-indicator-container {
display: flex;
align-items: center;
justify-content: flex-end;
margin-left: auto;
width: 8rem;
}
&.green {
background-image: url($svg-shop-list-button-green-availability);
}
.availability-indicator {
background-repeat: no-repeat;
display: flex;
align-items: center;
padding: 0.125rem 0.5rem;
&.orange {
background-image: url($svg-shop-list-button-orange-availability);
}
}
.button-auxillary-copy {
box-sizing: border-box;
justify-content: right;
line-height: 1.25rem;
font-weight: 500;
font-size: 0.75rem;
align-items: center;
}
.availability-badge {
display: inline-flex;
width: 13px;
height: 12px;
background-position: center;
background-repeat: no-repeat;
margin: 0 0.25rem 0 0;
&.green {
color: $green-700;
background-color: $green-100;
}
&.green {
background-image: url($svg-shop-list-button-green-availability);
}
&.orange {
color: $orange-600;
background-color: $orange-100;
}
&.orange {
background-image: url($svg-shop-list-button-orange-availability);
}
&.gray {
color: $gray-600;
background-color: $gray-100;
padding-left: 0.125rem;
padding-right: 0.125rem;
}
}
}
&.gray {
background-image: url(~@/assets/img/heritage-loader-blue.gif);
background-size: contain;
}
}
.button-auxillary-copy {
box-sizing: border-box;
justify-content: right;
line-height: 1.25rem;
font-weight: 500;
font-size: 0.75rem;
align-items: center;
}
.row-two {
text-align: left;
}
&.green {
color: $green-700;
background-color: $green-100;
}
&.orange {
color: $orange-600;
background-color: $orange-100;
}
&.gray {
color: $gray-600;
background-color: $gray-100;
padding-left: 0.125rem;
padding-right: 0.125rem;
}
}
}
.row-two {
text-align: center;
}
}
</style>

View file

@ -54,7 +54,7 @@ import siteFooter from '@/iss-components/site-footer/site-footer.vue';
import siteSubHeader from '@/iss-components/site-sub-header/site-sub-header.vue';
import buttonQuestion from '@/digital-components/button-question/button-question.vue';
import buttonMain from '@/ux-components/button-main/button-main.vue';
import buttonVariants from '@/constants/button-variants';
import { buttonVariants } from '@/constants/component-variants';
// Supporting files
import { fetchCmsContentForPage } from '@/helpers/cms-content-helper.js';
@ -135,7 +135,8 @@ export default {
* @summary Steps to perform when forward button clicked.
*/
async forwardButtonAction() {
if (this.selectedAnswer == null) {
// If user clicked foward without selecting an option or user click on "start a new claim" button.
if (this.selectedAnswer == null || this.selectedAnswer === 'NewClaim') {
this.navigateForward();
return;
}
@ -154,6 +155,7 @@ export default {
}
},
navigateForward() {
this.mainStore.updateDuplicateCheckVisited(true);
if (!this.mainStore.isPolicyLookupSuccessful) {
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD_POLICY_UNVERIFIED,

View file

@ -15,7 +15,7 @@
<siteSubHeader
ref="siteSubHeader"
cmsWidgetName="SiteSubHeaderWidget"
class="mt-4" />
class="site-sub-header-container" />
<buttonQuestion
v-if="educatorEndorsement"
ref="schoolPropertyQuestion"
@ -192,17 +192,147 @@ export default {
min-height: 1px;
padding-left: .9375rem;
padding-right: .9375rem;
.windshield-chip-count-question {
:deep(.col:first-of-type) {
.list-button-horizontal {
border-bottom-left-radius: $border-radius-list-button;
border-top-left-radius: $border-radius-list-button;
&:hover {
box-shadow: none;
}
.list-button-horizontal-content {
border-bottom-left-radius: $border-radius-list-button;
border-top-left-radius: $border-radius-list-button;
border-right-width: 0;
span {
font-size: 1rem;
font-weight: 400;
line-height: 1.5rem;
color: #525656;
}
}
&.selected {
.list-button-horizontal-content {
span {
color: #000;
font-weight: 600;
}
}
}
}
}
:deep(.col:last-of-type) {
.list-button-horizontal {
border-bottom-right-radius: $border-radius-list-button;
border-top-right-radius: $border-radius-list-button;
&:hover {
box-shadow: none;
}
.list-button-horizontal-content {
border-bottom-right-radius: $border-radius-list-button;
border-top-right-radius: $border-radius-list-button;
span {
font-size: 1rem;
font-weight: 400;
line-height: 1.5rem;
color: #525656;
}
}
&.selected {
.list-button-horizontal-content {
span {
font-weight: 600;
color: #000;
}
}
}
}
}
:deep(.list-button-horizontal) {
.list-button-horizontal-content {
padding: 0.625rem;
border: 1px solid #b3b4b5;
height: 2.875rem;
}
&.selected {
.list-button-horizontal-content {
border: 1px solid #0070d1;
box-shadow: none;
}
}
input[type="radio"] {
&:focus + .list-button-horizontal-content {
box-shadow: none;
border: 2.5px solid #0070d1;
outline: none;
}
}
&.has-error {
border: none;
.list-button-horizontal-content {
border: 1px solid #db0020;
}
input[type="radio"] {
&:focus + .list-button-horizontal-content {
border: 2.5px solid #db0020;
}
}
}
}
:deep(fieldset) {
box-shadow: 0 1px 5px rgba(0, 0, 0, .2);
border-radius: $border-radius-list-button;
}
}
:deep(.question-text) {
line-height: 1.5rem;
margin-bottom: .3125rem;
& > span {
text-align: left;
line-height: 1.5rem;
font-weight: 600;
}
}
}
}
:deep(.subheader-primary) {
margin-bottom: map-get($spacers, 2);
line-height: 1.5rem;
h5 {
line-height: 1.5rem;
span {
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
}
}
}
:deep(.subheader-secondary) {
line-height: 1.5rem;
margin-top: 0.625rem;
p {
font-size: $h6-font-size;
line-height: map-get($spacers, 5);
font-size: 1rem;
font-weight: 400;
line-height: 1.5rem;
}
}
@ -213,18 +343,6 @@ export default {
justify-content: left !important;
padding-left: 0;
padding-right: 0;
margin-bottom: map-get($spacers, 5);
}
:deep(.question-text) {
& > span {
text-align: left;
line-height: map-get($spacers, 5);
}
margin-bottom: map-get($spacers, 2);
}
:deep(div.question-text.d-flex) {
margin-top: map-get($spacers, 4);
}
</style>

View file

@ -127,7 +127,7 @@ describe('navigation', () => {
expect(wrapper.vm.navigateForward).toHaveBeenCalled();
});
test('if the coverage policy verified navigate forward to policy-vehicle page', async () => {
test('if the coverage policy verified navigate forward to vehicle damage page', async () => {
// Arrange
const mockRegistrationAddress = {
streetAddress: '1234 Main St',
@ -135,14 +135,6 @@ describe('navigation', () => {
state: 'OH',
zipCode: '43215'
};
const mockvehicles = [
{
vin: 'TEST_VIN'
},
{
vin: 'TEST_VIN2'
}
];
const { wrapper } = setupMocks();
@ -152,13 +144,12 @@ describe('navigation', () => {
customerQuestions: {
addressQuestions: mockRegistrationAddress
},
isCoverageEnabled: true,
vehiclesCount: 2,
vehiclesFound: mockvehicles
isCoverageEnabled: true
});
useMainStore().order.policy.policyNumber = 'p_0001';
useMainStore().order.policy.dateOfLoss = '2022-01-28';
useMainStore().order.vehicle.policyVehicleId = 0;
// Act
@ -167,10 +158,111 @@ describe('navigation', () => {
// Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith(
navigationScenarios.CLICKED_FORWARD_POLICY_VERIFIED,
undefined,
{},
{},
mockvehicles
undefined
);
});
test('if the coverage policy unverified navigate forward to vehicle damage page', async () => {
// Arrange
const mockRegistrationAddress = {
streetAddress: '1234 Main St',
city: 'Columbus',
state: 'OH',
zipCode: '43215'
};
const { wrapper } = setupMocks();
await wrapper.setData({
firstName: 'KK',
lastName: 'KK',
customerQuestions: {
addressQuestions: mockRegistrationAddress
},
isCoverageEnabled: true
});
useMainStore().order.policy.policyNumber = 'p_0001';
useMainStore().order.policy.dateOfLoss = '2022-01-28';
useMainStore().order.vehicle.policyVehicleId = null;
// Act
await wrapper.vm.forwardButtonAction();
// Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith(
navigationScenarios.CLICKED_FORWARD_POLICY_UNVERIFIED,
undefined
);
});
test('if the coverage policy verified navigate back to welcome page', async () => {
// Arrange
const mockRegistrationAddress = {
streetAddress: '1234 Main St',
city: 'Columbus',
state: 'OH',
zipCode: '43215'
};
const { wrapper } = setupMocks();
await wrapper.setData({
firstName: 'KK',
lastName: 'KK',
customerQuestions: {
addressQuestions: mockRegistrationAddress
},
isCoverageEnabled: true
});
useMainStore().order.policy.policyNumber = 'p_0001';
useMainStore().order.policy.dateOfLoss = '2022-01-28';
useMainStore().order.vehicle.policyVehicleId = 0;
// Act
await wrapper.vm.backButtonAction();
// Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith(
navigationScenarios.CLICKED_BACK_POLICY_VERIFIED,
undefined
);
});
test('if the coverage policy unverified navigate back to vehicle-selection page', async () => {
// Arrange
const mockRegistrationAddress = {
streetAddress: '1234 Main St',
city: 'Columbus',
state: 'OH',
zipCode: '43215'
};
const { wrapper } = setupMocks();
await wrapper.setData({
firstName: 'KK',
lastName: 'KK',
customerQuestions: {
addressQuestions: mockRegistrationAddress
},
isCoverageEnabled: true
});
useMainStore().order.policy.policyNumber = 'p_0001';
useMainStore().order.policy.dateOfLoss = '2022-01-28';
useMainStore().order.vehicle.policyVehicleId = null;
// Act
await wrapper.vm.backButtonAction();
// Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith(
navigationScenarios.CLICKED_BACK_POLICY_UNVERIFIED,
undefined
);
});
});

View file

@ -78,7 +78,7 @@
cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="!meta.valid"
@ForwardClicked="forwardButtonAction"
@backClicked="navigateBack" />
@backClicked="backButtonAction" />
</div>
</div>
</div>
@ -138,7 +138,6 @@ export default {
data() {
return {
customerQuestions: this.getPolicyHolderDetailsFromStore(),
vehiclesFound: [],
rules: {
firstName: globalRules.POLICYHOLDER_FIRST_NAME_REQUIRED,
lastName: globalRules.POLICYHOLDER_LAST_NAME_REQUIRED,
@ -152,8 +151,8 @@ export default {
isCoverageEnabled() {
return this.mainStore.issConfig.isCoverageEnabled;
},
vehiclesCount() {
return this.vehiclesFound.length;
isPolicyVehicleSelected() {
return (this.mainStore.order.vehicle.policyVehicleId != null && this.mainStore.order.vehicle.policyVehicleId >= 0);
}
},
methods: {
@ -163,13 +162,10 @@ export default {
},
navigateForward() {
if (this.vehiclesCount > 0) {
if (this.isPolicyVehicleSelected) {
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD_POLICY_VERIFIED,
this.$route,
{},
{},
this.vehiclesFound
this.$route
);
} else {
this.$router.navigate(
@ -179,6 +175,20 @@ export default {
}
},
backButtonAction() {
if (this.isPolicyVehicleSelected) {
this.$router.navigate(
this.navigationScenarios.CLICKED_BACK_POLICY_VERIFIED,
this.$route
);
} else {
this.$router.navigate(
this.navigationScenarios.CLICKED_BACK_POLICY_UNVERIFIED,
this.$route
);
}
},
getPolicyHolderDetailsFromStore() {
return {
addressQuestions: {
@ -194,7 +204,7 @@ export default {
lastName: this.mainStore.order.customer.lastName,
phoneNumber: this.mainStore.order.contactInfo.homePhone,
extension: this.mainStore.order.contactInfo.extension,
email: this.mainStore.order.customer.emailAddress,
email: this.mainStore.order.customer.emailAddress
};
}
}

View file

@ -70,7 +70,7 @@ import coverageType from '@/constants/coverage-type';
import alert from '@/ux-components/alert/alert.vue';
import siteSubHeader from '@/iss-components/site-sub-header/site-sub-header.vue';
import buttonMain from '@/ux-components/button-main/button-main.vue';
import buttonVariants from '@/constants/button-variants';
import { buttonVariants } from '@/constants/component-variants';
export default {
name: 'policy-vehicles',

View file

@ -29,7 +29,7 @@
v-html="scheduleWithOtherText"></div>
<textLink
class="underlined-text"
linkType="navigation"
linkType="text"
text="Find another shop"
href="#"
@clickEvent="findAnotherShopClicked" />

View file

@ -1,24 +1,21 @@
<template>
<transition
name="fade"
mode="out-in">
<div
v-if="isDisplayed"
class="appointment-type-question"
aria-live="polite">
<buttonQuestion
ref="buttonQuestion"
v-model="selectedValues"
customButtonQuestionId="appointmentTypeQuestion"
:questionText="questionText"
:answers="answersToDisplay"
:groupName="groupName"
buttonTypeString="listCard"
:suppressError="suppressError"
:validationRules="validationRules"
isRequired />
</div>
</transition>
<transition
name="fade"
mode="out-in">
<div
class="appointment-type-question"
aria-live="polite">
<buttonQuestion
ref="buttonQuestion"
v-model="selectedValues"
customButtonQuestionId="appointmentTypeQuestion"
:answers="answersToDisplay"
:groupName="groupName"
:suppressError="suppressError"
:validationRules="validationRules"
isRequired />
</div>
</transition>
</template>
<script>
@ -38,12 +35,9 @@ export default {
modelValue: String,
groupName: String,
isAvailable: Boolean,
isDisplayed: Boolean,
suppressError: Boolean,
validationRules: String,
cmsWidgetName: String,
isServiceableMobile: Boolean,
isServiceableInshop: Boolean
cmsWidgetName: String
},
emits: ['update:modelValue'],
computed: {
@ -51,23 +45,13 @@ export default {
return this.getCmsContent(this.cmsWidgetName, 'QuestionText');
},
answersFromCms() {
const retCms = this.getCmsContent(this.cmsWidgetName, 'Answers');
if (retCms && retCms.length > 0 && this.isMobileFeeHidden) {
return retCms.map((x) => (x.Name === AppointmentTypeStrings.MOBILE ? { ...x, SubText: x.SubText.replace('*', '') } : x));
}
return retCms;
return this.getCmsContent(this.cmsWidgetName, 'Answers');
},
answersToDisplay() {
const shouldShowMobile = this.isServiceableMobile && this.isItacOrNoComp;
const shouldShowMobileNotITACNotNoComp = this.isServiceableMobile && !this.isItacOrNoComp;
const shouldShowInshop = this.isServiceableInshop;
const shouldShowDropoff = this.isServiceableInshop && !useMainStore().damage.isRepair;
return this.answersFromCms
? this.answersFromCms.filter((answer) => (
(answer.Name === AppointmentTypeStrings.IN_SHOP && shouldShowInshop)
|| (answer.Name === AppointmentTypeStrings.MOBILE && shouldShowMobile)
|| (answer.Name === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP && shouldShowMobileNotITACNotNoComp)
|| (answer.Name === AppointmentTypeStrings.DROP_OFF && shouldShowDropoff)
(answer.Name === AppointmentTypeStrings.IN_SHOP)
|| (answer.Name === AppointmentTypeStrings.MOBILE)
))
: [];
},
@ -78,58 +62,13 @@ export default {
set(newValue) {
this.$emit('update:modelValue', newValue);
}
},
isITAC() {
return useMainStore().isITAC;
},
isItacOrNoComp() {
return this.isITAC || useMainStore().isNoComp;
},
isMobileFeeHidden() {
return this.getSettingValue(experimentSettings.ISS_FEATURE_TOGGLE_IS_MOBILE_FEE_HIDDEN) === 'true';
},
isMobileOnly() {
return this.isServiceableMobile && !this.isServiceableInshop;
}
},
watch: {
answersToDisplay: {
handler(newValue) {
// If there is only one option to display and that option is 'Mobile' then select it
if (
newValue.length === 1
&& newValue.findIndex((answer) => (answer.Name === AppointmentTypeStrings.MOBILE
|| answer.Name === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP)) !== -1
) {
if (this.isItacOrNoComp) {
this.selectedValues = AppointmentTypeStrings.MOBILE;
} else {
this.selectedValues = AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP;
}
}
},
immediate: true
},
isMobileOnly: {
handler(newValue) {
if (newValue) {
if (this.isItacOrNoComp) {
this.selectedValues = AppointmentTypeStrings.MOBILE;
} else {
this.selectedValues = AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP;
}
}
}
}
}
};
</script>
<style lang="scss" scoped>
:deep(.button-question) {
.list-card img {
height: auto;
width: 3.417rem;
}
.appointment-type-question {
margin-bottom: 1.25rem;
}
</style>

View file

@ -1,29 +1,13 @@
/* eslint-env jest */
import baseMixin from '@/mixins/base-mixin';
import { shallowMount } from '@vue/test-utils';
import { getMountOptions } from '@/helpers/unit-test-helper.js';
import { mount, flushPromises } from '@vue/test-utils';
import { createTestingPinia } from '@pinia/testing';
import { AppointmentTypeStrings } from '@/constants/schedule-constants';
import navigationScenarios from '@/router/router-constants/navigation-scenarios';
import routerParams from '@/router/router-constants/router-params';
import { useMainStore } from '@/store';
import serviceLocation from '@/layouts/service-location/service-location.vue';
// Define Mocks
jest.mock('@/helpers/cms-content-helper', () => ({
fetchCmsContentForPage: jest.fn(() => Promise.resolve('content'))
}));
/** @ignore */
function setupMocks() {
const wrapper = shallowMount(
serviceLocation,
getMountOptions({
router: {
navigate: jest.fn()
}
})
);
wrapper.vm.$router.navigateWithSpinner = jest.fn();
wrapper.vm.navigateBack = baseMixin.methods.navigateBack;
return { wrapper };
}
import { getZipCodeData } from '@/helpers/service-location-helper';
const mockGetServiceabilityDetails = () => {
const serviceabilityDetails = {
@ -35,123 +19,199 @@ const mockGetServiceabilityDetails = () => {
return Promise.resolve(serviceabilityDetails);
};
const mockZipcodeData = (zip) => {
if (zip === '43235' || zip === '55555') {
return Promise.resolve({
containsMilitaryBase: false,
isValid: true,
isServiceable: true,
city: 'Columbus',
state: 'OH',
zipCodeCtu: '01820'
});
}
if (zip === '45433') {
return Promise.resolve({
containsMilitaryBase: true,
isValid: true,
isServiceable: true,
city: 'Columbus',
state: 'OH',
zipCodeCtu: '01820'
});
}
return Promise.resolve({
containsMilitaryBase: false,
isValid: false,
isServiceable: false,
city: null,
state: null,
zipCodeCtu: null
});
}
const mockProviders = () => {
return Promise.resolve([
{
"address": {
"city": "COLUMBUS",
"country": "US",
"state": "OH",
"streetAddress": "6826 Sawmill Rd",
"streetAddress2": "",
"zipCode": "43235",
"zipCodeCtu": "03357"
},
"distanceInMiles": 4.136335989015438,
"providerNumber": "003357",
"companyName": "SAFELITE AUTOGLASS - COLUMBUS, OH",
"phoneNumber": "6142336400",
"isSafeliteShop": true
},
{
"address": {
"city": "Lewis Center",
"country": "US",
"state": "OH",
"streetAddress": "1343 Cameron Ave",
"streetAddress2": "",
"zipCode": "43035",
"zipCodeCtu": "03357"
},
"distanceInMiles": 8.193072412262042,
"providerNumber": "003417",
"companyName": "SAFELITE AUTOGLASS - LEWIS CENTER, OH",
"phoneNumber": "6147815433",
"isSafeliteShop": true
}
])
}
jest.mock(
'@/helpers/service-location-helper',
() => ({
getServiceabilityDetails: jest.fn((mockServiceZipCode) => mockGetServiceabilityDetails(mockServiceZipCode))
getServiceabilityDetails: jest.fn((mockServiceZipCode) => mockGetServiceabilityDetails(mockServiceZipCode)),
getZipCodeData: jest.fn((mockServiceZipCode) => mockZipcodeData(mockServiceZipCode)),
getMobileZipCodeData: jest.fn((mockServiceZipCode) => mockZipcodeData(mockServiceZipCode))
})
);
const mockMixin = {
methods: {
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
// linkWidgetName is not defined. This whole spec file needs review.
if (widgetName === linkWidgetName) {
return mockLinkCmsContent[cmsFieldName];
const mockRoute = {
params: {}
};
const mockRouter = {
navigate: jest.fn()
};
const mountOptions = {
global: {
mixins: [
{
computed: {
navigationScenarios() {
return navigationScenarios;
},
routerParams() {
return routerParams;
}
},
methods: {
getCmsContent: jest.fn(),
getFooterInfoBoxHeight: jest.fn(() => 80),
getPageNameByQueryString: jest.fn(() => '')
}
}
if (widgetName === modalWidgetName) {
return mockModalCmsContent[cmsFieldName];
}
return null;
}),
getZipCodeData: jest.fn((zip) => {
if (zip === '43235' || zip === '55555') {
return Promise.resolve({
containsMilitaryBase: false,
isValid: true,
state: 'OH',
zipCodeCtu: '01820'
});
}
if (zip === '45433') {
return Promise.resolve({
containsMilitaryBase: true,
isValid: true,
state: 'OH'
});
}
return Promise.resolve({
containsMilitaryBase: false,
isValid: false,
state: null,
zipCodeCtu: null
});
}),
onSubmit: jest.fn(),
onInvalidSubmit: jest.fn()
],
mocks: {
$route: mockRoute,
$router: mockRouter
},
stubs: {
alert: true,
textBlock: true,
appointmentTypeQuestion: true,
contentGroupModal: true,
siteFooter: true,
siteHeader: true,
siteSubHeader: true,
shopAddress: true,
serviceZipQuestion: true
}
}
};
function setupMocks({ appointmentType = AppointmentTypeStrings.IN_SHOP }) {
mountOptions.global.plugins = [createTestingPinia({
initialState: {
main: {
order: {
serviceLocation: {
appointmentType: appointmentType
}
}
}
}
})];
useMainStore().getProviders = jest.fn().mockImplementation(() => mockProviders());
const wrapper = mount(serviceLocation, mountOptions);
describe('navigation', () => {
test('if the back button is clicked, navigate back', async () => {
// Arrange
const { wrapper } = setupMocks({
wrapper.vm.$router.navigateWithSpinner = jest.fn();
wrapper.vm.navigateBack = baseMixin.methods.navigateBack;
return { wrapper };
}
beforeEach(() => {
jest.clearAllMocks();
});
describe('service-location.vue', () => {
describe('navigation', () => {
test('if the back button is clicked, navigate back', async () => {
// Arrange
const { wrapper } = setupMocks({
});
// Act
await wrapper.vm.navigateBack();
// Assert
expect(wrapper.vm.$router.navigateWithSpinner).toHaveBeenCalled();
});
});
describe('updating service zip', () => {
test('updates the page model after changing the service zip code', () => {
// Arrange
const { wrapper } = setupMocks({ appointmentType: AppointmentTypeStrings.IN_SHOP });
const newServiceZipCode = '61606';
const shopAddressComponent = wrapper.findComponent({
ref: 'shopAddress'
});
// Act
shopAddressComponent.vm.$emit('zip-updated', newServiceZipCode);
// Assert
expect(wrapper.vm.zipCode).toStrictEqual(newServiceZipCode);
});
// Act
await wrapper.vm.navigateBack();
test('displays military zip message when zip is updated', async () => {
// Arrange
const { wrapper } = setupMocks({ appointmentType: AppointmentTypeStrings.MOBILE });
// Assert
expect(wrapper.vm.$router.navigateWithSpinner).toHaveBeenCalled();
});
});
describe('updating service zip', () => {
test('updates the page model after changing the service zip code', () => {
// Arrange
const { wrapper } = setupMocks({
mixins: [mockMixin]
});
const newServiceZipCodeQuestion = {
zipCode: '61606',
state: 'IL'
};
const serviceZipCodeComponent = wrapper.findComponent({
ref: 'serviceZipCodeQuestion'
});
// Act
serviceZipCodeComponent.vm.$emit('update:modelValue', newServiceZipCodeQuestion);
// Assert
expect(wrapper.vm.serviceZipCodeQuestion).toStrictEqual(newServiceZipCodeQuestion);
});
test('displays military zip message when zip is updated', () => {
// Arrange
const { wrapper } = setupMocks({});
const mobileLocationQuestionsComponent = wrapper.findComponent({
ref: 'mobileLocationQuestions'
});
mobileLocationQuestionsComponent.resetComponent = jest.fn();
const serviceZipCodeComponent = wrapper.findComponent({
ref: 'serviceZipCodeQuestion'
});
serviceZipCodeComponent.resetMobileFeePart = jest.fn();
expect(wrapper.vm.zipContainsMilitaryBase).toBe(false);
// TODO: Use or remove
const newServiceZipCodeQuestion = {
zipCode: '45433',
state: 'OH'
};
// Act
serviceZipCodeComponent.vm.$emit('updated-contains-military-base', true);
// Assert
expect(wrapper.vm.zipContainsMilitaryBase).toBe(true);
const mobileServiceZipCodeQuestion = wrapper.findComponent({
ref: 'mobileServiceZipCodeQuestion'
});
expect(wrapper.vm.zipContainsMilitaryBase).toBe(false);
const newMobileServiceZipCode = '45433'
// Act
mobileServiceZipCodeQuestion.vm.$emit('update:modelValue', newMobileServiceZipCode);
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.zipContainsMilitaryBase).toBe(true);
});
});
});

View file

@ -12,28 +12,7 @@
<div class="service-location-container iss-heritage-content-container-width">
<siteSubHeader
cmsWidgetName="SiteSubHeaderWidget"
class="mt-4" />
<serviceZipModalQuestion
ref="serviceZipCodeQuestion"
v-model="serviceZipCodeQuestion"
modalWidgetName="ServiceZipModalWidget"
:onZipUpdateCallback="reloadShopData"
@updatedServiceability="setServiceabilityDetails"
@updatedContainsMilitaryBase="
setContainsMilitaryBase
" />
<alert
v-if="displayMilitaryZipAlert"
ref="alertMilitaryBaseZip"
class="my-5"
cmsWidgetName="AlertMilitaryBaseZipWidget"
alertClass="alert-warning" />
<alert
v-if="displayServiceableMobileOnly"
ref="alertMobileOnly"
class="my-5"
cmsWidgetName="AlertMobileOnlyWidget"
alertClass="alert-warning" />
class="subheader" />
<alert
v-if="displayRecalibrationWarning"
ref="alertRecalNoMobile"
@ -41,18 +20,6 @@
cmsWidgetName="AlertRecalNoMobileWidget"
alertClass="alert-warning"
@text-link-clicked="openModalAction" />
<alert
v-if="displayServiceableInshopOnly"
ref="alertInshopOnly"
class="my-5"
cmsWidgetName="AlertInshopOnlyWidget"
alertClass="alert-warning" />
<alert
v-if="displayNoShopsAlert"
ref="alertNoShops"
class="my-5"
cmsWidgetName="AlertNoShopsWidget"
alertClass="alert-warning" />
<alert
v-if="displayBigTruckNoShops"
ref="alertBigTruckNoShops"
@ -60,41 +27,94 @@
cmsWidgetName="AlertBigTruckNoShopsWidget"
alertClass="alert-warning" />
<div class="appointment-type">
<div class="appointment-type-question-text d-flex">
<span class="w-100 recal-text" v-if="recalibrationRequired">{{ scheduleRecalText }}</span>
<span class="w-100" v-if="!requiresInshopRecalibration">{{ appointmentQuestionText }}</span>
</div>
<alert
v-if="displayServiceableInshopOnly"
ref="alertInshopOnly"
cmsWidgetName="AlertInshopOnlyWidget"
:manualCopy="inShopOnlyCopy"
:isCollapsible="true"
alertClass="alert-warning" />
<alert
v-if="displayServiceableMobileOnly"
ref="alertMobileOnly"
cmsWidgetName="AlertMobileOnlyWidget"
:manualCopy="mobileOnlyCopy"
:isCollapsible="true"
alertClass="alert-warning" />
<appointmentTypeQuestion
v-show="isAppointmentTypeDisplayed"
v-if="!requiresInshopRecalibration && isServiceableMobile"
ref="appointmentTypeQuestion"
v-model="selectedAppointmentType"
:isServiceableMobile="isServiceableMobile"
:isServiceableInshop="isServiceableInshop"
:isDisplayed="isAppointmentTypeDisplayed"
groupName="appointmentTypeQuestion"
cmsWidgetName="AppointmentTypeQuestionWidget"
validationRules="option-required" />
</div>
<mobileLocationModalQuestions
v-if="isMobileLocationDisplayed"
ref="mobileLocationQuestions"
v-model="mobileLocationQuestions"
customComponentId="mobileLocationQuestions"
:mobileFeePart="mobileFeePart"
validationRules="mobile-location-required"
linkWidgetName="MobileLocationLinkWidget"
modalWidgetName="MobileLocationModalWidget"
:onZipUpdateCallback="reloadShopData"
@updated-mobile-fee-part="setMobileFeePart"
@updated-serviceability="setServiceabilityDetails"
@updated-contains-military-base="setContainsMilitaryBase"
@updated-mobile-ctu="setCtuForMobile" />
<shopQuestion
v-show="isShopQuestionDisplayed"
:ref="SHOP_QUESTION_REF_NAME"
v-model="selectedProvider"
:selectedAppointmentType="selectedAppointmentType"
:isDisplayed="isShopQuestionDisplayed"
cmsWidgetName="ShopQuestionWidget"
@updatedMobileProviderNumber="
setMobileProviderNumber
" />
<div
v-if="isInshop">
<alert
v-if="displayLowAvailabilityInshop"
ref="alertLowAvailabilityInshop"
cmsWidgetName="AlertLowAvailabilityInshopWidget"
:isCollapsible="true"
alertClass="alert-warning" />
<shopAddress
ref="shopAddress"
v-model="selectedProvider"
:serviceZipcode="zipCode"
:selectedAppointmentType="selectedAppointmentType"
modalWidgetName="ChangeLocationModalWidget"
cmsWidgetName="ShopAddressWidget"
@zip-updated="handleInShopZipUpdated" />
<!-- INTEGRATION TODO: Put this right under the calendar -->
<alert
v-if="selectedProvider === null"
ref="alertNoAvailableShops"
cmsWidgetName="AlertNoAvailableShopsWidget"
:isCollapsible="true"
alertClass="alert-danger" />
</div>
<div
v-if="isMobile">
<div class="expandable-link-container">
<a
v-if="displayMilitaryZipAlert && !militaryBaseWarningExpanded"
href="#"
class="expandable-link"
@click.prevent="militaryBaseWarningExpanded = true">
{{ militaryBaseWarningLinkText }}
</a>
<div
v-if="displayMilitaryZipAlert && militaryBaseWarningExpanded"
class="expandable-link-text">
{{ militaryBaseWarningText }}
</div>
</div>
<serviceZipQuestion
ref="mobileServiceZipCodeQuestion"
v-model="mobileZipCode"
customInputId="mobileServiceZipCode"
class="mobile-service-zip-question"
:placeholderText="mobileZipPlaceholder"
:serviceZipFormatErrorMessage="errorMessages.MOBILE_SERVICE_ZIP_FORMAT"
:hasError="mobileZipError !== ''"
cmsWidgetName="MobileZipWidget" />
<div
v-if="mobileZipError"
ref="errorMessageDiv"
class="row my-1 form-test-error">
<span
class="d-inline-flex mt-0"
role="alert">{{ mobileZipError }}</span>
</div>
<textBlock
v-if="displayMobileFeeDisclaimer"
cmsWidgetName="MobileFeeDisclaimerWidget"
typeStyle="disclaimer" />
</div>
<contentGroupModal
:ref="RECAL_MODAL_REF_NAME"
cssModalHeadlineClass="text-center"
@ -104,7 +124,7 @@
class="mt-5"
cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="
!meta.valid || displayNoShopsAlert
!meta.valid || displayNoShopsAlert || displayBigTruckNoShops
"
@backClicked="navigateBack(this, navigateBackScenario)"
@forwardClicked="forwardButtonAction" />
@ -118,44 +138,31 @@
import { AppointmentTypeStrings } from '@/constants/schedule-constants.js';
import { fetchCmsContentForPage } from '@/helpers/cms-content-helper';
import settleAllPromises from '@/helpers/layout-helper';
import { required } from '@/helpers/validation-rules';
import errorMessages from '@/constants/error-messages';
import { useMainStore } from '@/store';
import {
getPricedMobileFeePart,
getServiceabilityDetails,
getZipCodeData
getZipCodeData,
getMobileZipCodeData
} from '@/helpers/service-location-helper';
import { toTitleCase } from '@/helpers/text-helper.js';
import { getAvailabilityRating } from '@/helpers/service-location-helper';
// Import Component
import alert from '@/ux-components/alert/alert.vue';
import textBlock from '@/digital-components/text-block/text-block.vue';
import appointmentTypeQuestion from '@/layouts/service-location/appointment-type-question/appointment-type-question.vue';
import baseFormMixin from '@/mixins/base-form-mixin';
import contentGroupModal from '@/iss-components/content-group-modal/content-group-modal.vue';
import mobileLocationModalQuestions from '@/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue';
import { Form, defineRule } from 'vee-validate';
import shopQuestion from '@/layouts/service-location/shop-question/shop-question.vue';
import siteFooter from '@/iss-components/site-footer/site-footer.vue';
import siteHeader from '@/iss-components/site-header/site-header.vue';
import siteSubHeader from '@/iss-components/site-sub-header/site-sub-header.vue';
import serviceZipModalQuestion from '@/layouts/service-location/service-zip-modal-question/service-zip-modal-question.vue';
import shopAddress from '@/layouts/service-location/shop-address/shop-address.vue';
import serviceZipQuestion from '@/layouts/service-location/service-zip-question/service-zip-question.vue';
import widgetFields from '@/constants/cms-widget-fields';
// DEFINE VALIDATION RULES
defineRule('mobile-location-required', (value) => {
if (
value.addressQuestions.streetAddress === ''
|| value.addressQuestions.city === ''
|| value.addressQuestions.state === ''
|| value.addressQuestions.zipCode === ''
|| value.isVehicleProtected == null
) {
return errorMessages.MOBILE_LOCATION_REQUIRED;
}
return true;
});
defineRule('selection-required', required(errorMessages.OPTION_REQUIRED));
const SHOP_QUESTION_REF_NAME = 'shopQuestion';
const RECAL_MODAL_REF_NAME = 'RecalModal';
const SITE_FOOTER_REF_NAME = 'siteFooter';
@ -163,16 +170,16 @@ export default {
name: 'service-location',
components: {
alert,
textBlock,
appointmentTypeQuestion,
contentGroupModal,
mobileLocationModalQuestions,
siteFooter,
siteHeader,
siteSubHeader,
// eslint-disable-next-line vue/no-reserved-component-names
Form,
serviceZipModalQuestion,
shopQuestion
shopAddress,
serviceZipQuestion
},
mixins: [baseFormMixin],
async beforeRouteEnter(to, from, next) {
@ -182,8 +189,8 @@ export default {
const zipCodeData = getZipCodeData(serviceZipCode);
const mobileFeePartPromise = getPricedMobileFeePart(serviceZipCode);
const serviceabilityDetailsPromise = getServiceabilityDetails(serviceZipCode);
const shopQuestionInitialDataPromise = shopQuestion.methods.loadInitialData(serviceZipCode);
const getGlassFeesPromise = useMainStore().getGlassFees();
const providersPromise = useMainStore().getProviders(serviceZipCode);
// Settle promises and get results
const promiseResultMap = [
@ -203,13 +210,13 @@ export default {
resultKey: 'serviceabilityDetails',
promise: serviceabilityDetailsPromise
},
{
resultKey: 'shopQuestionInitialData',
promise: shopQuestionInitialDataPromise
},
{
resultKey: 'zipCodeData',
promise: zipCodeData
},
{
resultKey: 'providers',
promise: providersPromise
}
];
@ -221,10 +228,9 @@ export default {
resultMap.zipCodeData,
resultMap.serviceabilityDetails,
resultMap.mobileFeePart,
resultMap.shopQuestionInitialData?.mobileProviderNumber,
resultMap.glassFees
resultMap.glassFees,
resultMap.providers
);
vm.$refs[SHOP_QUESTION_REF_NAME].initializeComponent(resultMap.shopQuestionInitialData);
});
},
setup() {
@ -238,7 +244,6 @@ export default {
city: this.getServiceCityFromStore(),
state: this.getServiceStateFromStore(),
zipCode: this.getServiceZipCodeFromStore(),
isVehicleProtected: this.getIsVehicleProtectedFromStore(),
isGlassServiceableInshop: null,
isRecalibrationServiceableInshop: null,
isGlassServiceableMobile: null,
@ -249,9 +254,13 @@ export default {
mobileProviderNumber: null,
zipContainsMilitaryBase: false,
zipCodeCtu: null,
SHOP_QUESTION_REF_NAME,
mobileZipCode: '',
mobileZipError: '',
militaryBaseWarningExpanded: false,
availabilityRating: null,
RECAL_MODAL_REF_NAME,
SITE_FOOTER_REF_NAME
SITE_FOOTER_REF_NAME,
errorMessages
};
},
computed: {
@ -264,61 +273,6 @@ export default {
answersFromCms() {
return this.getCmsContent('ServiceTypeQuestionWidget', 'Answers');
},
serviceZipCodeQuestion: {
get() {
return {
state: this.state,
zipCode: this.zipCode
};
},
set(newValue) {
if (newValue.zipCode !== this.zipCode) {
this.resetMobileLocation();
this.selectedAppointmentType = null;
this.selectedProvider = null;
}
this.state = newValue.state;
this.zipCode = newValue.zipCode;
// eslint-disable-next-line vue/valid-next-tick
this.$nextTick();
}
},
mobileLocationQuestions: {
get() {
return {
addressQuestions: {
streetAddress: this.streetAddress,
streetAddress2: this.streetAddress2,
city: this.city,
state: this.state,
zipCode: this.zipCode
},
isVehicleProtected: this.isVehicleProtected
};
},
set(newValue) {
this.streetAddress = newValue.addressQuestions.streetAddress;
this.streetAddress2 = newValue.addressQuestions.streetAddress2;
this.city = newValue.addressQuestions.city;
this.state = newValue.addressQuestions.state;
this.zipCode = newValue.addressQuestions.zipCode;
this.isVehicleProtected = newValue.isVehicleProtected;
if (newValue.zipCode !== this.zipCode) {
if (
!(
this.selectedAppointmentType === AppointmentTypeStrings.MOBILE
|| this.selectedAppointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP
)
) {
this.selectedAppointmentType = null;
}
this.selectedProvider = null;
}
}
},
isServiceableMobile() {
if (this.isRecalibrationServiceableMobile !== null) {
return (
@ -338,22 +292,14 @@ export default {
return this.isGlassServiceableInshop;
},
isShopQuestionDisplayed() {
isInshop() {
return (
this.selectedAppointmentType === 'Inshop'
|| this.selectedAppointmentType === 'Dropoff'
);
},
isAppointmentTypeDisplayed() {
return this.zipCode && !this.displayNoShopsAlert;
},
isMobileLocationDisplayed() {
return (
this.selectedAppointmentType
=== AppointmentTypeStrings.MOBILE
|| this.selectedAppointmentType
=== AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP
);
isMobile() {
return this.selectedAppointmentType === AppointmentTypeStrings.MOBILE
},
requiresInshopRecalibration() {
// Specifically check for isRecalibrationServiceableMobile === false, not null or true.
@ -375,15 +321,17 @@ export default {
displayRecalibrationWarning() {
return this.requiresInshopRecalibration;
},
displayLowAvailabilityInshop() {
return this.availabilityRating === 'low' && this.isInshop;
},
displayServiceableInshopOnly() {
return (
!this.displayRecalibrationWarning
&& this.isServiceableInshop
&& !this.isServiceableMobile
);
return !this.isServiceableMobile && this.isServiceableInshop && !this.displayRecalibrationWarning;
},
displayServiceableMobileOnly() {
return this.isServiceableMobile && !this.isServiceableInshop;
return this.isServiceableMobile && this.recalibrationRequired && !this.isServiceableInshop;
},
displayMobileFeeDisclaimer() {
return this.mainStore.isNoComp || this.mainStore.isITAC;
},
navigateBackScenario() {
const { isNoComp, isITAC } = useMainStore();
@ -393,6 +341,38 @@ export default {
},
isBigTruck() {
return this.mainStore.order.vehicle.isBigTruck;
},
mobileZipPlaceholder() {
return this.getCmsContent('MobileZipPlaceHolderWidget', widgetFields.TEXT_BLOCK_WIDGET.TEXT);
},
militaryBaseWarningLinkText() {
return this.getCmsContent('AlertMilitaryBaseZipWidget', widgetFields.ALERT_WIDGET.HEADLINE_TEXT);
},
militaryBaseWarningText() {
return this.getCmsContent('AlertMilitaryBaseZipWidget', widgetFields.ALERT_WIDGET.BODY_TEXT);
},
appointmentQuestionText() {
return this.getCmsContent('WhereWouldYouLikeServiceWidget', widgetFields.CONTENT_GROUP_WIDGET.HEADER_TEXT);
},
scheduleRecalText() {
if(this.requiresInshopRecalibration) {
return this.getCmsContent('WhereWouldYouLikeServiceWidget', widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT_2);
} else {
return this.getCmsContent('WhereWouldYouLikeServiceWidget', widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT);
}
},
inShopOnlyCopy() {
let content = this.getCmsContent('AlertInshopOnlyWidget', widgetFields.ALERT_WIDGET.BODY_TEXT);
content = content.replace('{custom:city}', toTitleCase(this.city));
return content;
},
mobileOnlyCopy() {
let content = this.getCmsContent('AlertMobileOnlyWidget', widgetFields.ALERT_WIDGET.BODY_TEXT);
content = content.replace('{custom:city}', toTitleCase(this.city));
return content;
},
recalibrationRequired() {
return this.mainStore.hasRecalibrationPart;
}
},
methods: {
@ -401,13 +381,10 @@ export default {
useMainStore().order.serviceLocation.zipCode !== null
);
},
async reloadShopData(zipCode) {
await this.$refs[SHOP_QUESTION_REF_NAME].reloadShopData(zipCode);
},
async forwardButtonAction() {
let provider = this.selectedProvider;
this.mainStore.updateMobileFee(null);
if (this.isMobileLocationDisplayed) {
if (this.isMobile) {
if (this.mainStore.isNoComp || this.mainStore.isITAC) {
this.mainStore.updateMobileFee(this.mobileFeePart);
}
@ -432,7 +409,6 @@ export default {
zipCode: this.zipCode,
zipCodeCtu: this.zipCodeCtu,
appointmentType: this.selectedAppointmentType,
isVehicleProtected: this.isVehicleProtected,
provider
});
@ -466,9 +442,6 @@ export default {
|| useMainStore().order.customer.address.zipCode
);
},
getIsVehicleProtectedFromStore() {
return useMainStore().order.serviceLocation.isVehicleProtected;
},
getSelectedAppointmentType() {
return useMainStore().order.serviceLocation.appointmentType;
},
@ -482,12 +455,13 @@ export default {
zipCodeData,
serviceabilityDetails,
mobileFeePart,
mobileProviderNumber,
glassFees
glassFees,
providers
) {
if (zipCodeData) {
this.zipContainsMilitaryBase = zipCodeData.containsMilitaryBase;
this.zipCodeCtu = zipCodeData.zipCodeCtu;
this.city = zipCodeData.city;
}
if (serviceabilityDetails) {
@ -498,8 +472,24 @@ export default {
this.mobileFeePart = mobileFeePart;
}
if (mobileProviderNumber) {
this.setMobileProviderNumber(mobileProviderNumber);
if (providers) {
this.setMobileProviderNumber(providers.mobileProviderNumber);
let foundMatch = false;
if(this.selectedProvider && this.selectedProvider.providerNumber) {
const matchedProvider = providers.shopProviders.find(
(provider) => provider.providerNumber === this.selectedProvider.providerNumber
);
if (matchedProvider) {
foundMatch = true;
this.selectedProvider = matchedProvider;
}
}
if(providers.shopProviders.length > 0 && !foundMatch) {
this.selectedProvider = providers.shopProviders[0];
}
else if(providers.shopProviders.length === 0) {
this.selectedProvider = null;
}
}
if (glassFees) {
@ -521,13 +511,6 @@ export default {
setMobileProviderNumber(providerNumber) {
this.mobileProviderNumber = providerNumber;
},
resetMobileLocation() {
this.streetAddress = '';
this.streetAddress2 = '';
this.city = '';
this.isVehicleProtected = null;
},
setServiceabilityDetails(serviceabilityDetails) {
this.isGlassServiceableInshop =
serviceabilityDetails.isGlassServiceableInshop;
@ -537,14 +520,94 @@ export default {
serviceabilityDetails.isGlassServiceableMobile;
this.isRecalibrationServiceableMobile =
serviceabilityDetails.isRecalibrationServiceableMobile;
if (!this.isServiceableMobile) {
this.selectedAppointmentType = AppointmentTypeStrings.IN_SHOP;
}
},
async updateMobileZip() {
this.mobileZipError = '';
const zipCodeData = await getMobileZipCodeData(this.mobileZipCode);
if (!zipCodeData.isValid) {
this.mobileZipError = errorMessages.INVALID_ZIP;
} else if (!zipCodeData.isServiceable) {
this.mobileZipError = errorMessages.NO_SERVICE_IN_AREA(toTitleCase(zipCodeData.city));
} else {
this.city = zipCodeData.city;
this.zipCode = this.mobileZipCode;
this.setCtuForMobile(zipCodeData.zipCodeCtu);
this.setContainsMilitaryBase(zipCodeData.containsMilitaryBase);
const serviceabilityDetailsPromise = getServiceabilityDetails(this.mobileZipCode);
serviceabilityDetailsPromise.then((result) => {
const details = result.data;
if (details) {
this.setServiceabilityDetails(details);
}
});
const providersPromise = useMainStore().getProviders(this.mobileZipCode);
providersPromise.then((result) => {
const providers = result.data;
if (providers) {
this.setMobileProviderNumber(providers.mobileProviderNumber);
if(providers.shopProviders.length > 0) {
this.selectedProvider = providers.shopProviders[0];
}
else {
this.selectedProvider = null;
}
}
});
}
},
async handleInShopZipUpdated(newZip) {
this.zipCode = newZip;
const zipCodeData = await getZipCodeData(this.zipCode);
this.city = zipCodeData.city;
this.setCtuForMobile(zipCodeData.zipCodeCtu);
this.setContainsMilitaryBase(zipCodeData.containsMilitaryBase);
const serviceabilityDetailsPromise = getServiceabilityDetails(this.zipCode);
serviceabilityDetailsPromise.then((result) => {
const details = result.data;
if (details) {
this.setServiceabilityDetails(details);
}
});
}
},
watch: {
mobileZipCode(newZip) {
if(newZip !== '') {
this.updateMobileZip();
}
},
selectedAppointmentType() {
this.mobileZipCode = '';
if(this.selectedAppointmentType === AppointmentTypeStrings.IN_SHOP && this.selectedProvider) {
const startDate = new Date();
const endDate = new Date();
endDate.setDate(startDate.getDate() + 6);
const formattedStartDate = startDate.toISOString().split('T')[0];
const formattedEndDate = endDate.toISOString().split('T')[0];
getAvailabilityRating(
formattedStartDate,
formattedEndDate,
AppointmentTypeStrings.IN_SHOP,
this.selectedProvider ? this.selectedProvider.providerNumber : null
).then((rating) => {
this.availabilityRating = rating;
});
}
}
}
};
</script>
<style lang="scss">
<style lang="scss" scoped>
$page-side-padding: 1.5rem;
.iss-heritage-container-width {
.service-location-container {
position: relative;
@ -554,58 +617,41 @@ $page-side-padding: 1.5rem;
}
}
.question-text {
span {
line-height: 1.5rem;
text-align: center;
.service-location-container {
.subheader {
margin: 1.25rem 0;
}
}
.list-card-content p {
&:first-of-type {
line-height: 1.5rem;
.form-test-error {
font-weight: $font-weight-bold;
}
&:not(:nth-of-type(1)) {
line-height: 1.25rem;
}
}
.choose-option {
.button-question > div {
&:first-of-type {
margin-bottom: 0.95rem;
line-height: 1.5rem;
.appointment-type-question-text {
color: $black;
font-weight: 600;
margin-bottom: 1rem;
font-size: 1rem;
line-height: 1.625rem;
flex-direction: column;
.recal-text {
margin-bottom: 1.25rem;
font-weight: $font-weight-bold;
}
}
}
.button-question {
.row.form-test-error {
line-height: 1.5rem;
padding-left: 0rem !important;
}
}
.appointment-type-question {
.row {
margin-top: 0;
.col-12 {
margin: 0 0 .5rem 0;
&:last-child {
margin-bottom: 0;
}
.mobile-service-zip-question {
:deep(.form-test-error span) {
font-weight: $font-weight-bold;
}
}
.question-text {
margin: 0;
span {
padding: 1rem 0;
text-align: center;
}
.expandable-link-container {
margin-bottom: 1rem;
}
}
.service-location-button-question {
.question-text {
margin-top: 1.5rem;
span {
text-align: center;
.expandable-link {
font-style: italic;
font-weight: $font-weight-bold;
text-decoration: none;
color: $heritage-blue-primary;
line-height: 1.375rem;
&:hover {
text-decoration: underline;
}
}
}

View file

@ -35,7 +35,7 @@
<script>
import textLink from '@/ux-components/text-link/text-link.vue';
import serviceZipQuestion from '@/layouts/service-location/service-zip-modal-question/service-zip-question/service-zip-question.vue';
import serviceZipQuestion from '@/layouts/service-location/service-zip-question/service-zip-question.vue';
import modal from '@/digital-components/modal/modal.vue';
import alert from '@/ux-components/alert/alert.vue';

View file

@ -1,51 +0,0 @@
<template>
<textboxQuestion
ref="zipInputTextQuestion"
v-model="value"
:cmsWidgetName="cmsWidgetName"
inputId="serviceZipCode"
questionAlignment="center"
cornerStyle="rounded"
mask="#####"
:displayQuestionText="false"
isRequired
validationRules="zip-required|zip-format" />
</template>
<script>
import textboxQuestion from '@/digital-components/textbox-question/textbox-question.vue';
// Supporting Files
import { defineRule } from 'vee-validate';
import { required, regex } from '@/helpers/validation-rules';
import errorMessages from '@/constants/error-messages';
// Define Validation Rules
defineRule('zip-required', required(errorMessages.SERVICE_ZIP_REQUIRED));
defineRule('zip-format', regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT));
export default {
name: 'service-zip-question',
components: {
textboxQuestion
},
// TODO: Correct prop def.
props: {
modelValue: {
serviceZipCode: String
},
cmsWidgetName: String
},
emits: ['update:modelValue'],
computed: {
value: {
get() {
return this.modelValue;
},
set(newValue) {
this.$emit('update:modelValue', newValue);
}
}
}
};
</script>

View file

@ -1,10 +1,10 @@
import { shallowMount } from '@vue/test-utils';
import serviceZipQuestion from '@/layouts/service-location/service-zip-modal-question/service-zip-question/service-zip-question.vue';
import serviceZipQuestion from '@/layouts/service-location/service-zip-question/service-zip-question.vue';
describe('service-zip-question.vue', () => {
it('Should get the modelValue', async () => {
// Arrange
const text = 'test';
const text = '12345';
const wrapper = shallowMount(serviceZipQuestion, {
props: {
modelValue: text
@ -13,16 +13,16 @@ describe('service-zip-question.vue', () => {
});
// Act
const modelValueText = wrapper.vm.value;
wrapper.vm.value = 'test also';
const modelValueText = wrapper.vm.internalZipcode;
wrapper.vm.internalZipcode = '55555';
// Assert
expect(modelValueText).toEqual('test');
expect(modelValueText).toEqual('12345');
});
it('Should emit to set value', async () => {
// Arrange
const text = 'test';
const text = '12345';
const wrapper = shallowMount(serviceZipQuestion, {
props: {
modelValue: text
@ -31,9 +31,10 @@ describe('service-zip-question.vue', () => {
});
// Act
wrapper.vm.value = 'test also';
wrapper.vm.internalZipcode = '55555';
wrapper.vm.updateModelValue();
// Assert
expect(wrapper.emitted('update:modelValue')).toEqual([['test also']]);
expect(wrapper.emitted('update:modelValue')).toEqual([['55555']]);
});
});

View file

@ -0,0 +1,74 @@
<template>
<textboxQuestion
ref="zipInputTextQuestion"
v-model="internalZipcode"
:cmsWidgetName="cmsWidgetName"
inputId="serviceZipCode"
cornerStyle="rounded"
mask="#####"
isRequired
:hasError="hasError"
@clickEvent="updateModelValue"
:validationRules="`zip-required|${cmsWidgetName}-zip-format`" />
</template>
<script>
import textboxQuestion from '@/digital-components/textbox-question/textbox-question.vue';
// Supporting Files
import { defineRule } from 'vee-validate';
import { required, regex } from '@/helpers/validation-rules';
import errorMessages from '@/constants/error-messages';
// Define Validation Rules
defineRule('zip-required', required(errorMessages.SERVICE_ZIP_REQUIRED));
export default {
name: 'service-zip-question',
components: {
textboxQuestion
},
// TODO: Correct prop def.
props: {
modelValue: String,
cmsWidgetName: String,
serviceZipFormatErrorMessage: {
type: String,
default: errorMessages.SERVICE_ZIP_FORMAT
},
hasError: {
type: Boolean,
default: false
}
},
data() {
return {
internalZipcode: this.modelValue
}
},
emits: ['update:modelValue'],
methods: {
updateModelValue() {
this.$emit('update:modelValue', this.internalZipcode);
}
},
watch: {
modelValue(newValue) {
this.internalZipcode = newValue;
}
},
mounted() {
defineRule(`${this.cmsWidgetName}-zip-format`, regex(/^\d{5}$/, this.serviceZipFormatErrorMessage));
}
};
</script>
<style lang="scss" scoped>
.textbox-question.has-success {
:deep(input.form-control) {
border: $border-input;
&:focus {
border: $border-input-focus;
}
}
}
</style>

View file

@ -0,0 +1,411 @@
<template>
<transition
name="fade"
mode="out-in">
<div
class="shop-address"
aria-live="polite">
<div class="address-header">{{ questionText }}</div>
<div
v-if="modelValue"
class="current-address">
<div class="address-line">
<span class="shop-name">{{ displayedShopName }}</span>
<span class="shop-distance">{{ modelValue.distanceInMiles?.toFixed(2) }} mi</span>
</div>
<div class="address-line">{{ getProviderAddress(modelValue) }}</div>
<div class="address-line">
{{ getProviderCityZipState(modelValue) }}
</div>
</div>
<div
v-else
class="no-shops-found">
{{ `No shops found within 100 miles of ${internalZipcode}` }}
</div>
<buttonMain
class="more-shops-button"
:buttonText="showMoreShopsLinkText"
:variant="'primary'"
@clickEvent="openModal" />
<alert
class="alert-shop-distance"
v-if="modelValue?.distanceInMiles > 30"
ref="alertShopDistance"
cmsWidgetName="AlertShopDistanceWidget"
alertClass="alert-warning" />
<modal
:ref="modalName"
:headerText="modalHeaderText"
:onModalOpenedCallback="onModalOpened"
:footerButtonText="modalFooterText"
:modalPosition="modalPositions.center"
:allowInvalidSubmit="true"
@footerButtonEvent="updateSelectedProvider">
<googleMap
id="map"
class="mb-4"
:markers="providerAddresses"
:zipCode="internalZipcode" />
<dropdownQuestion
inputId="searchRadiusDropdown"
ref="searchRadiusQuestion"
v-model="searchRadiusInMiles"
:cmsWidgetName="searchRadiusQuestionWidgetName"
:variant="dropdownVariants.compact"
:options="searchRadiusOptions" />
<buttonQuestion
ref="shopListButtonQuestion"
v-model="internalProviderNumber"
buttonTypeString="shopListButton"
:buttonTypeObject="shopListButton"
class="radioQuestion"
:answers="nearbyShopsData"
groupName="chooseShop"
textPosition="text-start"
isRequired
validationRules="option-required"
:additionalButtonData="additionalButtonData" />
<span class="service-zip-prompt">
{{ serviceZipPrompt }}
</span>
<serviceZipQuestion
v-if="renderServiceZip"
:ref="SERVICE_ZIP_QUESTION_REF_NAME"
v-model="internalZipcode"
customInputId="serviceZipCode"
:cmsWidgetName="textboxQuestionWidgetName" />
<div
v-if="errorMessage"
ref="errorMessageDiv"
class="row my-1 form-test-error">
<span
class="d-inline-flex mt-0"
role="alert">{{ errorMessage }}</span>
</div>
</modal>
</div>
</transition>
</template>
<script>
// Components
import alert from '@/ux-components/alert/alert.vue';
import buttonMain from '@/ux-components/button-main/button-main.vue';
import modal from '@/digital-components/modal/modal.vue';
import serviceZipQuestion from '@/layouts/service-location/service-zip-question/service-zip-question.vue';
import buttonQuestion from '@/digital-components/button-question/button-question.vue';
import shopListButton from '@/iss-components/shop-list-button/shop-list-button.vue';
import dropdownQuestion from '@/digital-components/dropdown-question/dropdown-question.vue';
import googleMap from '@/iss-components/google-map/google-map.vue';
// Supporting files
import baseMixin from '@/mixins/base-mixin.js';
import { toTitleCase } from '@/helpers/text-helper.js';
import { markRaw } from 'vue';
import { getAvailabilityRating } from '@/helpers/service-location-helper';
import { useMainStore } from '@/store';
import widgetFields from '@/constants/cms-widget-fields';
import { dropdownVariants, modalPositions } from '@/constants/component-variants';
import errorMessages from '@/constants/error-messages';
import { defineRule } from 'vee-validate';
import { required } from '@/helpers/validation-rules';
defineRule('option-required', required(errorMessages.PROVIDER_REQUIRED));
const SERVICE_ZIP_QUESTION_REF_NAME = 'serviceZipCodeQuestion';
export default {
name: 'shop-address',
components: {
alert,
buttonMain,
modal,
serviceZipQuestion,
buttonQuestion,
dropdownQuestion,
googleMap
},
mixins: [baseMixin],
props: {
modelValue: {
type: Object,
default: () => null
},
serviceZipcode: String,
cmsWidgetName: String,
validationRules: String,
modalWidgetName: String,
selectedAppointmentType: String,
},
data() {
return {
toTitleCase,
serviceZipCodeTextInputId: '',
internalZipcode: this.serviceZipcode,
internalProviderNumber: this.modelValue?.providerNumber,
searchRadiusQuestionWidgetName: 'SearchRadiusQuestionWidget',
textboxQuestionWidgetName: 'ChangeLocationZipQuestionWidget',
shopListButton: markRaw(shopListButton),
searchRadiusInMiles: "25",
nearbyShops: [],
modalPositions,
dropdownVariants,
renderServiceZip: true,
errorMessage: '',
openingModal: false,
SERVICE_ZIP_QUESTION_REF_NAME
};
},
emits: ['update:modelValue', 'zip-updated'],
computed: {
questionText() {
return this.getCmsContent(this.cmsWidgetName, widgetFields.INPUT_QUESTION_WIDGET.QUESTION_TEXT);
},
showMoreShopsLinkText() {
return this.getCmsContent('ShowMoreShopsLinkWidget', widgetFields.TEXT_BLOCK_WIDGET.TEXT);
},
displayedShopName() {
if(this.modelValue.isSafeliteShop) {
return this.getCmsContent('SafeliteShopNameWidget', widgetFields.TEXT_BLOCK_WIDGET.TEXT);
}
return toTitleCase(this.modelValue.companyName);
},
modalHeaderText() {
return this.getCmsContent(this.modalWidgetName, widgetFields.CONTENT_GROUP_WIDGET.HEADER_TEXT).replaceAll('{custom:serviceZipcode}', this.internalZipcode);
},
modalFooterText() {
return this.getCmsContent(this.modalWidgetName, widgetFields.CONTENT_GROUP_WIDGET.FOOTER_TEXT);
},
modalName() {
return this.modalWidgetName;
},
additionalButtonData() {
const startDate = new Date();
const endDate = new Date();
endDate.setDate(startDate.getDate() + 6);
const formattedStartDate = startDate.toISOString().split('T')[0];
const formattedEndDate = endDate.toISOString().split('T')[0];
return {
displayAvailabilityIndicators: true,
availabilityRatingCallback: getAvailabilityRating,
startDate: formattedStartDate,
endDate: formattedEndDate,
shopAppointmentType: "InshopOrDropoff"
};
},
serviceZipPrompt() {
return this.getCmsContent(this.modalWidgetName, widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT_2);
},
nearbyShopsData() {
return this.nearbyShops.map((shopProvider) => {
const city = toTitleCase(shopProvider.address.city);
const distanceInMiles = shopProvider.distanceInMiles.toFixed(2);
return {
buttonLabel: city,
buttonLabelSubCopy: `${distanceInMiles} mi`,
buttonBodyCopy: this.getFullProviderAddress(shopProvider),
value: shopProvider.providerNumber
};
});
},
searchRadiusArray() {
const options = this.getCmsContent(this.searchRadiusQuestionWidgetName, widgetFields.INPUT_QUESTION_WIDGET.ANSWERS);
return options;
},
searchRadiusOptions() {
const optionsObj = {};
if(this.searchRadiusArray) {
this.searchRadiusArray.forEach(option => {
optionsObj[option.Name] = option.Text;
});
}
return optionsObj;
},
providerAddresses() {
return this.nearbyShops?.map((provider) => ({
title: toTitleCase(provider.companyName),
fullAddress: this.getFullProviderAddress(provider),
addressLines: [this.getProviderAddress(provider), this.getProviderCityZipState(provider)]
})) ?? [];
},
},
watch: {
async internalZipcode() {
if(this.openingModal) {
return;
}
const shopsUpdated = await this.updateShops();
if (shopsUpdated) {
await this.$nextTick();
this.$refs[SERVICE_ZIP_QUESTION_REF_NAME].internalZipcode = '';
}
},
async searchRadiusInMiles() {
if(this.openingModal) {
return;
}
await this.updateShops();
}
},
methods: {
async updateShops(autoExpand = false) {
this.errorMessage = '';
let result = await this.getNearbyShops(this.searchRadiusInMiles);
let currentSearchIndex = this.searchRadiusArray.findIndex(option => option.Name === this.searchRadiusInMiles);
while (autoExpand && result.length === 0 && currentSearchIndex < this.searchRadiusArray.length - 1) {
const newSearchRadius = this.searchRadiusArray[currentSearchIndex + 1].Name;
result = await this.getNearbyShops(newSearchRadius);
currentSearchIndex++;
}
if (result.length === 0) {
this.errorMessage = errorMessages.ZIP_CODE_NOT_SERVICED_FOR_VEHICLE;
}
else {
if (!result.find(shop => shop.providerNumber === this.internalProviderNumber)) {
this.internalProviderNumber = '';
await this.$nextTick();
}
this.nearbyShops = result;
this.searchRadiusInMiles = this.searchRadiusArray[currentSearchIndex].Name;
return true;
}
},
openModal() {
this.$refs[this.modalName].openModal();
},
closeModal() {
this.$refs[this.modalName].closeModal();
},
onModalOpened() {
this.openingModal = true;
this.internalZipcode = this.serviceZipcode;
this.searchRadiusInMiles = "25";
this.nearbyShops = [];
this.updateShops(true).then(() => {
this.internalProviderNumber = this.nearbyShops[0]?.providerNumber;
}).finally(() => {
this.$nextTick().then(() => {
this.openingModal = false;
});
});
this.forceServiceZipRerender();
},
async updateSelectedProvider() {
const selectedShop = this.nearbyShops.find(shop => shop.providerNumber === this.internalProviderNumber);
if (selectedShop) {
this.$emit('update:modelValue', selectedShop);
this.$emit('zip-updated', this.internalZipcode);
this.closeModal();
}
},
async getNearbyShops(radiusInMiles) {
const result = await useMainStore().getProviders(this.internalZipcode, radiusInMiles);
return result.data.shopProviders;
},
getFullProviderAddress(provider) {
const addressLine1 = this.getProviderAddress(provider);
const addressLine2 = this.getProviderCityZipState(provider);
const joinString =
addressLine1.length > 0 && addressLine2.length > 0 ? ', ' : '';
return [addressLine1, addressLine2].join(joinString);
},
getProviderAddress(provider) {
return toTitleCase(provider?.address?.streetAddress);
},
getProviderCityZipState(provider) {
const city = toTitleCase(provider?.address?.city);
const state = provider?.address?.state ?? '';
const zipCode = provider?.address?.zipCode ?? '';
let addressLine2 = '';
if (city) {
addressLine2 += city;
if (state || zipCode) {
addressLine2 += state ? ', ' : ' ';
}
}
if (state) {
addressLine2 += state;
addressLine2 += zipCode ? ' ' : '';
}
if (zipCode) {
addressLine2 += zipCode;
}
return addressLine2;
},
forceServiceZipRerender() {
this.renderServiceZip = false;
this.$nextTick(() => {
this.renderServiceZip = true;
});
},
}
};
</script>
<style lang="scss" scoped>
@import "@/styles/ux-variables-svg-strings.scss";
.shop-address {
.address-header {
color: $black;
font-weight: 600;
margin-bottom: 0.625rem;
}
.address-line {
display: flex;
}
.shop-distance {
margin-left: auto;
}
.current-address {
margin: .625rem 0;
}
.no-shops-found {
font-size: .75rem;
margin: .75rem 0rem;
font-weight: $font-weight-bold;
line-height: 1.375rem;
}
.more-shops-button {
margin-bottom: 1.25rem;
}
.alert-shop-distance {
margin-top: 2.5rem;
}
:deep(.modal-title) {
font-weight: 400;
display: block;
width: fit-content;
}
.radioQuestion {
margin-bottom: .625rem;
}
.service-zip-prompt {
color: $black;
font-weight: 600;
}
:deep(#map) {
height: 27rem;
}
:deep(.textbox-question) {
&.has-error {
input.form-control {
border: $border-input;
&:focus {
border: $border-input-focus;
}
}
}
.form-label {
font-weight: $font-weight-normal;
color: $darker-gray;
}
.btn-primary {
margin-bottom: 1.25rem;
}
}
}
</style>

View file

@ -1,294 +0,0 @@
<template>
<transition
name="fade"
mode="out-in">
<div
v-if="isDisplayed"
class="shop-question"
aria-live="polite">
<alert
v-if="displayDropoffInformation"
ref="alertDropoffInformation"
class="mb-4 drop-off-alert"
cmsWidgetName="AlertDropoffInformationWidget"
alertClass="alert-info"
:isDismissible="false" />
<buttonQuestion
ref="buttonQuestion"
v-model="selectedProviderNumber"
buttonTypeString="shopListButton"
:buttonTypeObject="shopListButton"
class="radioQuestion"
:questionText="questionText"
:answers="answers"
groupName="chooseShop"
textPosition="text-start"
isRequired
validationRules="option-required"
:additionalButtonData="additionalButtonData" />
<textLink
v-show="displaySeeMoreLocationsLink"
id="showMoreShopsId"
ref="showMoreShopsLink"
class="show-more-shops-link"
cmsWidgetName="ShowMoreShopsLinkWidget"
linkType="text"
:text="showMoreShopsLinkText"
href="#!"
:aria-label="showMoreShopsLinkText"
@clickEvent="getNextShopsFromList" />
</div>
</transition>
</template>
<script>
// Components
import alert from '@/ux-components/alert/alert.vue';
import buttonQuestion from '@/digital-components/button-question/button-question.vue';
import textLink from '@/ux-components/text-link/text-link.vue';
// Supporting files
import { AppointmentTypeStrings } from '@/constants/schedule-constants.js';
import baseMixin from '@/mixins/base-mixin.js';
import { defineRule } from 'vee-validate';
import { required } from '@/helpers/validation-rules';
import errorMessages from '@/constants/error-messages';
import { useMainStore } from '@/store/index.js';
import { markRaw, nextTick } from 'vue';
import { getAvailabilityRating } from '@/helpers/service-location-helper';
import shopListButton from '@/iss-components/shop-list-button/shop-list-button.vue';
defineRule('option-required', required(errorMessages.OPTION_REQUIRED));
export default {
name: 'shop-question',
components: {
alert,
buttonQuestion,
textLink
},
mixins: [baseMixin],
props: {
modelValue: {
type: Object,
default: () => null
},
selectedAppointmentType: String,
cmsWidgetName: String,
validationRules: String,
isDisplayed: Boolean
},
emits: ['update:modelValue', 'updatedMobileProviderNumber'],
data() {
return {
shopProviders: [],
shopListButton: markRaw(shopListButton),
answers: [],
shopIndex: 0,
displaySeeMoreLocationsLink: false
};
},
computed: {
questionText() {
return this.getCmsContent(this.cmsWidgetName, 'QuestionText');
},
selectedValue: {
get() {
return this.modelValue;
},
set(newValue) {
this.$emit('update:modelValue', newValue);
}
},
selectedProviderNumber: {
get() {
return this.selectedValue?.providerNumber;
},
set(newValue) {
// Button Question only supports Number, or String data types so we must get the full object to emit
this.selectedValue = this.getSelectedProviderObject(newValue);
}
},
displayDropoffInformation() {
return this.selectedAppointmentType === 'Dropoff';
},
showMoreShopsLinkText() {
return this.getCmsContent('ShowMoreShopsLinkWidget', 'Text');
},
additionalButtonData() {
const startDate = new Date();
const endDate = new Date();
endDate.setDate(startDate.getDate() + 6);
const formattedStartDate = startDate.toISOString().split('T')[0];
const formattedEndDate = endDate.toISOString().split('T')[0];
return {
displayAvailabilityIndicators: true,
availabilityRatingCallback: getAvailabilityRating,
startDate: formattedStartDate,
endDate: formattedEndDate,
shopAppointmentType: this.selectedAppointmentType
};
}
},
watch: {
selectedAppointmentType: {
async handler(newValue) {
this.resetAnswers();
await nextTick();
this.selectedProviderNumber = null;
await nextTick();
if (newValue !== AppointmentTypeStrings.MOBILE && newValue !== AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP) {
this.getNextShopsFromList();
}
}
},
shopProviders: {
async handler(newValue) {
await nextTick();
if (this.selectedAppointmentType) {
const selectedShopIndex = this.getSelectedProviderIndex(
newValue,
this.selectedProviderNumber
);
if (selectedShopIndex >= 3) {
await this.getNextShopsFromList(selectedShopIndex + 1);
} else {
await this.getNextShopsFromList();
await nextTick();
}
}
}
}
},
methods: {
loadInitialData(serviceZipCode) {
return this.loadData(serviceZipCode);
},
loadData(serviceZipCode) {
return useMainStore().getProviders(serviceZipCode);
},
initializeComponent(shopQuestionInitialData) {
this.shopProviders = shopQuestionInitialData.shopProviders;
},
async getNextShopsFromList(numberToGet = 3) {
const shopIterator = (array, n) => {
const l = array.length;
return () => {
const end = this.shopIndex + n;
const part = array.slice(this.shopIndex, end);
this.shopIndex = end < l ? end : this.shopProviders.length;
return part;
};
};
const toTitleCase = (str) => str.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase());
const nextShop = shopIterator(this.shopProviders, numberToGet);
// Map API result data
const mappedData = nextShop().map((shopProvider) => {
const streetAddress = toTitleCase(shopProvider.address.streetAddress);
const city = toTitleCase(shopProvider.address.city);
const { state } = shopProvider.address;
const { zipCode } = shopProvider.address;
const distanceInMiles = Math.round(shopProvider.distanceInMiles * 2) / 2;
return {
buttonLabel: city,
buttonLabelSubCopy: `${distanceInMiles} mi`,
buttonBodyCopy: `${streetAddress}, ${city}, ${state} ${zipCode}`,
value: shopProvider.providerNumber
};
});
if (this.answers.length === 0) {
this.answers = mappedData;
} else {
mappedData.forEach((shop) => {
this.answers.push(shop);
});
}
await nextTick();
if (this.shopIndex === this.shopProviders.length) {
this.displaySeeMoreLocationsLink = false;
} else {
this.displaySeeMoreLocationsLink = true;
}
await nextTick();
this.scrollToPageBottom();
},
resetAnswers() {
this.answers = [];
this.shopIndex = 0;
},
async reloadShopData(serviceZipCode) {
const result = await this.loadData(serviceZipCode);
this.initializeComponent(result.data);
if (result.data?.mobileProviderNumber
&& (this.selectedAppointmentType === AppointmentTypeStrings.MOBILE
|| this.selectedAppointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP)) {
this.$emit('updatedMobileProviderNumber', result.data.mobileProviderNumber);
}
this.resetAnswers();
await nextTick();
await this.getNextShopsFromList();
},
getSelectedProviderObject(providerNumber) {
const provider = this.shopProviders?.find((p) => p.providerNumber === providerNumber);
return provider;
},
getSelectedProviderIndex(providers, selectedProviderNumber) {
const index = providers.findIndex((p) => p.providerNumber === selectedProviderNumber);
return index;
}
}
};
</script>
<style lang="scss">
@import "@/styles/ux-variables-svg-strings.scss";
.shop-question {
margin-top: 1rem;
text-align: center;
.button-question {
.question-text {
margin-top: 0.5rem;
}
}
.drop-off-alert {
background-image: url($svg-drop-off-alert);
background-repeat: no-repeat;
background-size: 0.75rem;
background-position: 0.5rem 0.75rem;
border-radius: 0.5rem;
display: flex;
flex-direction: row;
padding: 0.5rem 0.5rem 0.5rem 1.5rem !important;
gap: 0.25rem;
.alert-heading {
text-align: left;
font-size: 0.75rem;
line-height: 1.25rem;
}
}
}
</style>

View file

@ -2,24 +2,20 @@
exports[`tpa-submit returns the initial data 1`] = `
Object {
"companyName": "Frederick Jones",
"customValueMap": Object {
"glassShop": "Frederick Jones",
"modalPositions": Object {
"center": "center",
"edge": "edge",
},
"sections": Array [],
"widget": Object {
"damageLocations": "DamageLocationsWidget",
"alertIncomplete": "AlertIncompleteWidget",
"alertRecalWarning": "AlertRecalWarningWidget",
"contactDetails": "ContactDetailsSectionWidget",
"editShopLinkText": "EditShopLinkTextWidget",
"footer": "SiteFooterWidget",
"orderDetails": "OrderDetailsContent",
"serviceSummary": "ServiceSummaryContent",
"siteHeader": "SiteHeaderWidget",
"siteSubHeader": "SiteSubHeaderWidget",
"subheader": Object {
"contactInfo": "ContactDetailsSubTitle",
"damage": "DamageSubTitle",
"shop": "PreferredShopSubTitle",
"vehicle": "VehicleSubTitle",
},
},
}
`;

View file

@ -3,12 +3,18 @@
exports[`contact-details-drawer snapshot matches returns the initial data 1`] = `
Object {
"emailAddress": "fred.tay@gmail.com",
"extension": null,
"firstName": "Frederick",
"isModalOpened": false,
"lastName": "Taylor",
"modalPositions": Object {
"center": "center",
"edge": "edge",
},
"phoneNumber": "606-009-2943",
"rules": Object {
"emailAddress": "email-required|email-address-format",
"extension": "extension-format",
"firstName": "first-name-required",
"lastName": "last-name-required",
"phoneNumber": "phone-number-required|phone-number-format",
@ -16,6 +22,7 @@ Object {
"widget": Object {
"drawerFooter": "ContactDetailsDrawerFooterWidget",
"emailQuestion": "EmailQuestionWidget",
"extensionQuestion": "ExtensionQuestionWidget",
"firstNameQuestion": "FirstNameQuestionWidget",
"lastNameQuestion": "LastNameQuestionWidget",
"phoneNumberQuestion": "PhoneNumberQuestionWidget",

View file

@ -65,15 +65,16 @@ describe('contact-details-drawer', () => {
describe('method', () => {
describe('saveContactDetails', () => {
test.each([
['Sarah', 'Jones', 's.jones@gmail.com', '724-996-0909'],
[null, 'Jones', 's.jones@gmail.com', '724-996-0909'],
['Sarah', null, 's.jones@gmail.com', '724-996-0909'],
['Sarah', 'Jones', null, '724-996-0909'],
['Sarah', 'Jones', 's.jones@gmail.com', null]
['Sarah', 'Jones', 's.jones@gmail.com', '724-996-0909', '12345'],
[null, 'Jones', 's.jones@gmail.com', '724-996-0909', '12345'],
['Sarah', null, 's.jones@gmail.com', '724-996-0909', '12345'],
['Sarah', 'Jones', null, '724-996-0909', '12345'],
['Sarah', 'Jones', 's.jones@gmail.com', null, '12345'],
['Sarah', 'Jones', 's.jones@gmail.com', '724-996-0909', null],
])(
'when first name data "%p", last name "%p", email "%p", and phone number "%p", updateContactInfo called with expected',
(firstName, lastName, emailAddress, phoneNumber) => {
'when first name data "%p", last name "%p", email "%p", phone number "%p", and extension "%p", updateContactInfo called with expected',
(firstName, lastName, emailAddress, phoneNumber, extension) => {
// Arrange
const mainInitialState = {
order: {
@ -81,12 +82,13 @@ describe('contact-details-drawer', () => {
firstName: 'Frederick',
lastName: 'Taylor',
emailAddress: 'fred.tay@gmail.com',
servicePhone: '606-009-2943'
servicePhone: '606-009-2943',
extension: '11111'
}
}
};
const initialData = {
firstName, lastName, emailAddress, phoneNumber
firstName, lastName, emailAddress, phoneNumber, extension
};
const { wrapper } = getMountedComponent(mainInitialState, initialData);
@ -98,7 +100,7 @@ describe('contact-details-drawer', () => {
expect(useMainStore().updateContactInfo).toBeCalledTimes(1);
expect(useMainStore().updateContactInfo).toBeCalledWith({ firstName, lastName, emailAddress });
expect(useMainStore().updatePhoneNumbers).toBeCalledTimes(1);
expect(useMainStore().updatePhoneNumbers).toBeCalledWith({ home: phoneNumber, service: phoneNumber });
expect(useMainStore().updatePhoneNumbers).toBeCalledWith({ home: phoneNumber, service: phoneNumber, extension: extension });
}
);
});

View file

@ -1,6 +1,7 @@
<template>
<modal
:ref="modalName"
:modalPosition="modalPositions.center"
:headerText="headerText"
:footerButtonText="footerButtonText"
:onModalClosedCallback="resetFormValues"
@ -8,25 +9,27 @@
@isModalOpened="setModalStatus"
@footerButtonEvent="clickFooterButtonEvent">
<template v-if="isModalOpened">
<textboxQuestion
ref="firstNameQuestion"
v-model="firstName"
inputId="firstName"
:cmsWidgetName="widget.firstNameQuestion"
isRequired
:validationRules="rules.firstName" />
<textboxQuestion
ref="lastNameQuestion"
v-model="lastName"
class="mt-4"
inputId="lastName"
:cmsWidgetName="widget.lastNameQuestion"
isRequired
:validationRules="rules.lastName" />
<div class="row">
<textboxQuestion
class="col"
ref="firstNameQuestion"
v-model="firstName"
inputId="firstName"
:cmsWidgetName="widget.firstNameQuestion"
isRequired
:validationRules="rules.firstName" />
<textboxQuestion
class="col"
ref="lastNameQuestion"
v-model="lastName"
inputId="lastName"
:cmsWidgetName="widget.lastNameQuestion"
isRequired
:validationRules="rules.lastName" />
</div>
<textboxQuestion
ref="emailQuestion"
v-model="emailAddress"
class="mt-4"
inputId="emailAddress"
:cmsWidgetName="widget.emailQuestion"
isRequired
@ -34,13 +37,19 @@
<textboxQuestion
ref="phoneNumberQuestion"
v-model="phoneNumber"
class="mt-4"
inputId="phoneNumber"
:cmsWidgetName="widget.phoneNumberQuestion"
isRequired
:mask="phoneMask"
disableAutoFill
:validationRules="rules.phoneNumber" />
<textboxQuestion
ref="extensionQuestion"
v-model="extension"
inputId="extension"
:cmsWidgetName="widget.extensionQuestion"
disableAutoFill
:validationRules="rules.extension" />
</template>
</modal>
</template>
@ -51,10 +60,11 @@ import modal from '@/digital-components/modal/modal.vue';
import textboxQuestion from '@/digital-components/textbox-question/textbox-question.vue';
// Supporting Files
import { useMainStore } from '@/store/index.js';
import { useMainStore } from '@/store';
import globalRules from '@/constants/global-rules.js';
import MaskaFormattedMasks from '@/constants/maska-masks';
import widgetFields from '@/constants/cms-widget-fields.js';
import { modalPositions } from '@/constants/component-variants';
export default {
name: 'contact-details-drawer',
@ -67,27 +77,32 @@ export default {
const { firstName,
lastName,
emailAddress,
servicePhone } = useMainStore().contactInfo;
servicePhone,
extension } = useMainStore().contactInfo;
return {
isModalOpened: false,
firstName,
lastName,
emailAddress,
phoneNumber: servicePhone,
extension: extension,
widget: {
title: 'ContactDetailsDrawerHeaderWidget',
firstNameQuestion: 'FirstNameQuestionWidget',
lastNameQuestion: 'LastNameQuestionWidget',
emailQuestion: 'EmailQuestionWidget',
phoneNumberQuestion: 'PhoneNumberQuestionWidget',
extensionQuestion: 'ExtensionQuestionWidget',
drawerFooter: 'ContactDetailsDrawerFooterWidget'
},
rules: {
firstName: globalRules.FIRST_NAME_REQUIRED,
lastName: globalRules.LAST_NAME_REQUIRED,
emailAddress: `${globalRules.EMAIL_ADDRESS_REQUIRED}|${globalRules.EMAIL_ADDRESS_FORMAT}`,
phoneNumber: `${globalRules.PHONE_NUMBER_REQUIRED}|${globalRules.PHONE_NUMBER_FORMAT}`
}
phoneNumber: `${globalRules.PHONE_NUMBER_REQUIRED}|${globalRules.PHONE_NUMBER_FORMAT}`,
extension: `${globalRules.EXTENSION_FORMAT}`
},
modalPositions
};
},
computed: {
@ -121,10 +136,11 @@ export default {
lastName: this.lastName,
emailAddress: this.emailAddress
};
useMainStore().updateContactInfo(contactInfo);
useMainStore().updatePhoneNumbers({
this.mainStore.updateContactInfo(contactInfo);
this.mainStore.updatePhoneNumbers({
home: this.phoneNumber,
service: this.phoneNumber
service: this.phoneNumber,
extension: this.extension
});
this.$emit('update-contact-details');
},
@ -132,11 +148,13 @@ export default {
const { firstName,
lastName,
emailAddress,
servicePhone } = useMainStore().contactInfo;
servicePhone,
extension } = this.mainStore.contactInfo;
this.firstName = firstName;
this.lastName = lastName;
this.emailAddress = emailAddress;
this.phoneNumber = servicePhone;
this.extension = extension;
},
openModal() {
this.modal.openModal();
@ -155,5 +173,14 @@ export default {
font-size: $h5-font-size;
font-weight: $font-weight-normal !important;
}
.textbox-question {
margin-bottom: 1.25rem;
:deep(label:has(b)) {
font-weight: $font-weight-normal;
b {
font-weight: 600;
}
}
}
}
</style>

View file

@ -18,7 +18,7 @@ describe('deductible-box', () => {
it('renders the correct deductible label', () => {
const labelElement = wrapper.find('#deductibleLabel');
expect(labelElement.text()).toBe('Deductible');
expect(labelElement.text()).toBe('Deductible:');
});
it('has the correct id', () => {
@ -26,12 +26,8 @@ describe('deductible-box', () => {
});
it('has the correct classes', () => {
expect(wrapper.classes().length).toBe(5);
expect(wrapper.classes().length).toBe(1);
expect(wrapper.classes()).toContain('deductible-box');
expect(wrapper.classes()).toContain('d-flex');
expect(wrapper.classes()).toContain('justify-content-between');
expect(wrapper.classes()).toContain('align-items-center');
expect(wrapper.classes()).toContain('py-2');
});
it.each([
['', ''],

View file

@ -1,17 +1,9 @@
<template>
<div
id="deductibleBox"
class="deductible-box d-flex justify-content-between align-items-center py-2">
<p
id="deductibleLabel"
class="deductible-box__text">
Deductible
</p>
<p
id="deductibleValue"
class="deductible-box__text">
{{ value }}
</p>
class="deductible-box">
<span id="deductibleLabel" class="deductible-label">Deductible:</span>
<span id="deductibleValue" class="deductible-value">{{ value }}</span>
</div>
</template>
@ -28,12 +20,17 @@ export default {
<style lang="scss" scoped>
.deductible-box {
background-color: $green;
display: flex;
align-items: center;
gap: .25rem;
}
.deductible-box__text {
color: white;
.deductible-label {
color: $black;
font-weight: 600;
}
.deductible-value {
color: $green;
font-weight: 500;
margin: 0;
font-size: 1.625rem;
}
</style>

View file

@ -20,12 +20,12 @@ describe('ReviewBlock.vue', () => {
});
it('renders the correct number of lines', () => {
const lineElements = wrapper.findAll('.review-block__body--line');
const lineElements = wrapper.findAll('.review-body p');
expect(lineElements.length).toBe(lines.length);
});
it('renders the correct line text', () => {
const lineElements = wrapper.findAll('.review-block__body--line');
const lineElements = wrapper.findAll('.review-body p');
lines.forEach((line, index) => {
expect(lineElements.at(index).text()).toBe(line);
});

View file

@ -1,29 +1,28 @@
<template>
<div class="review-block">
<div class="d-flex justify-content-between">
<p class="review-block__header small-strong pb-1">
{{ customHeaderText }}
</p>
<textLink
linkType="textSmall"
:text="editLinkText"
useLoadingModal
href="javascript:void(0)"
class="link"
@clickEvent="editClicked">
<template
v-if="editScreenReaderTextCmsWidgetName"
#after-text>
<span class="sr-only"> {{ screenReaderOnlyText }} </span>
</template>
</textLink>
</div>
<p
v-for="line in lines"
:key="line"
class="small review-block__body--line"
v-html="line">
<p class="review-block__header">
{{ customHeaderText }}
</p>
<div class="review-body">
<p
v-for="line in lines"
:key="line"
v-html="line">
</p>
</div>
<textLink
linkType="text"
:text="editLinkText"
useLoadingModal
href="javascript:void(0)"
class="link"
@clickEvent="editClicked">
<template
v-if="editScreenReaderTextCmsWidgetName"
#after-text>
<span class="sr-only"> {{ screenReaderOnlyText }} </span>
</template>
</textLink>
</div>
</template>
@ -36,14 +35,13 @@ export default {
props: {
customHeaderText: String,
editScreenReaderTextCmsWidgetName: String,
lines: Array
lines: Array,
editLinkText: {
type: String,
default: 'Edit'
},
},
emits: ['click-edit'],
data() {
return {
editLinkText: 'Edit'
};
},
methods: {
editClicked() {
this.$emit('click-edit');
@ -59,12 +57,16 @@ export default {
}
.review-block__header {
color: $black;
font-weight: $font-weight-bold;
margin-bottom: .625rem;
}
.review-block__body--line {
color: $gray-600;
.review-body {
margin-bottom: .625rem
}
.link {
text-underline-offset: 1px;
font-weight: $font-weight-normal
}
}
</style>

View file

@ -18,7 +18,15 @@ import coverageStatuses from '@/constants/coverage-statuses';
jest.mock('@/helpers/cms-content-helper', () => ({
fetchCmsContentForPage: jest.fn(),
doesCopyContainRouterLink: jest.fn(),
getStringWithCustomValues: jest.fn(),
getStringWithCustomValues: jest.fn((str, customValueMap) => {
let newString = str ?? '';
if (customValueMap != null) {
Object.keys(customValueMap).forEach((key) => {
newString = newString.replaceAll(`{custom:${key}}`, customValueMap[key]);
});
}
return newString;
}),
processIfStatements: jest.fn()
}));
@ -129,11 +137,7 @@ describe('tpa-submit', () => {
// Assert
expect(subHeader.exists()).toBeTruthy();
expect(subHeader.props().justifyText).toBe('center');
expect(subHeader.props().marginTopSizeOverride).toBe(4);
expect(subHeader.classes()).toContain('text-color--black');
expect(subHeader.classes()).toContain('fs-5');
expect(subHeader.classes()).toContain('tpa-submit__title--line-height');
expect(subHeader.classes()).toContain('tpa-submit-title');
});
test('sub header body one', () => {
// Arrange
@ -144,34 +148,6 @@ describe('tpa-submit', () => {
// Assert
expect(subHeaderBodyOne.exists()).toBeTruthy();
expect(subHeaderBodyOne.classes()).toContain('small');
expect(subHeaderBodyOne.classes()).toContain('text-color--darker-gray');
});
test('sub header body two', () => {
// Arrange
const wrapper = shallowMount(tpaSubmit, getMountOptions());
// Act
const subHeaderBodyTwo = wrapper.findComponent({ ref: 'tpaSubmitSubHeaderBodyTwo' });
// Assert
expect(subHeaderBodyTwo.exists()).toBeTruthy();
expect(subHeaderBodyTwo.classes()).toContain('mb-4');
expect(subHeaderBodyTwo.classes()).toContain('small');
expect(subHeaderBodyTwo.classes()).toContain('text-color--darker-gray');
});
test('main button one', () => {
// Arrange
const wrapper = shallowMount(tpaSubmit, getMountOptions());
// Act
const mainButton = wrapper.findComponent({ ref: 'buttonMainOne' });
// Assert
expect(mainButton.exists()).toBeTruthy();
expect(mainButton.props().variant).toBe('success');
expect(mainButton.classes()).toContain('w-100');
expect(mainButton.classes()).toContain('mb-5');
});
test('service summary section', () => {
// Arrange
@ -183,21 +159,6 @@ describe('tpa-submit', () => {
// Assert
expect(serviceSummarySection.exists()).toBeTruthy();
});
test('service summary title', () => {
// Arrange
const wrapper = shallowMount(tpaSubmit, getMountOptions());
// Act
const serviceSummaryTitle = wrapper.findComponent({ ref: 'tpaSubmitServiceSummaryTitle' });
// Assert
expect(serviceSummaryTitle.exists()).toBeTruthy();
expect(serviceSummaryTitle.props().marginTopSizeOverride).toBe(4);
expect(serviceSummaryTitle.classes()).toContain('fw-bold');
expect(serviceSummaryTitle.classes()).toContain('fs-1');
expect(serviceSummaryTitle.classes()).toContain('lh-lg');
expect(serviceSummaryTitle.classes()).toContain('text-color--black');
});
describe('review blocks', () => {
const title1 = 'Section 1';
const title2 = 'Another Section';
@ -253,24 +214,8 @@ describe('tpa-submit', () => {
// Assert
expect(submitOrderDetailsTitle.exists()).toBeTruthy();
expect(submitOrderDetailsTitle.props().marginTopSizeOverride).toBe(4);
expect(submitOrderDetailsTitle.classes()).toContain('fw-bold');
expect(submitOrderDetailsTitle.classes()).toContain('text-color--black');
});
test('submit order details body', () => {
// Arrange
const wrapper = shallowMount(tpaSubmit, getMountOptions());
// Act
const submitOrderDetailsTitle = wrapper.findComponent({ ref: 'tpaSubmitOrderDetailsBody' });
// Assert
expect(submitOrderDetailsTitle.exists()).toBeTruthy();
expect(submitOrderDetailsTitle.props().marginTopSizeOverride).toBe(4);
expect(submitOrderDetailsTitle.classes()).toContain('mb-4');
expect(submitOrderDetailsTitle.classes()).toContain('px-4');
expect(submitOrderDetailsTitle.classes()).toContain('small');
expect(submitOrderDetailsTitle.classes()).toContain('text-color--darker-gray');
expect(submitOrderDetailsTitle.classes()).toContain('order-details-title');
});
test('deductible box', () => {
// Arrange
@ -306,7 +251,7 @@ describe('tpa-submit', () => {
});
});
describe('before route enter', () => {
test('produces 4 sections', async () => {
test('produces 2 sections', async () => {
// Arrange
const { wrapper } = getMountedComponent();
expect(wrapper.vm.sections.length).toBe(0);
@ -320,65 +265,13 @@ describe('tpa-submit', () => {
);
// Assert
expect(wrapper.vm.sections.length).toBe(4);
});
test.each([
['2004', 'Honda', 'Civic', '2004 Honda Civic'],
[null, 'Honda', 'Civic', 'Honda Civic'],
['2004', '', 'Civic', '2004 Civic'],
['2004', 'Honda', null, '2004 Honda'],
['2004', null, undefined, '2004'],
[null, null, null, '']
])(
'when store vehicle has year %p, make %p, and model %p, has line %p',
async (year, make, model, line) => {
// Arrange
const initialStore = {
order: {
vehicle: { year, make, model }
}
};
const { wrapper } = getMountedComponent(initialStore);
const vehicleSectionIndex = 0;
const expectedLines = [line];
// Act
await tpaSubmit.beforeRouteEnter.call(
wrapper.vm,
{ query: { issPage: 'tpa-submit' } },
undefined,
(c) => c(wrapper.vm)
);
// Assert
const vehicleSection = wrapper.vm.sections[vehicleSectionIndex];
expect(vehicleSection.lines).toStrictEqual(expectedLines);
}
);
test('damage section lines equal result from getDamageDisplayContent', async () => {
// Arrange
const { wrapper } = getMountedComponent();
const damageSectionIndex = 1;
const expectedLines = ['hi', 'potato', 'vehicle 3'];
getDamageDisplayContent.mockImplementationOnce(() => expectedLines);
// Act
await tpaSubmit.beforeRouteEnter.call(
wrapper.vm,
{ query: { issPage: 'tpa-submit' } },
undefined,
(c) => c(wrapper.vm)
);
// Assert
const damageSection = wrapper.vm.sections[damageSectionIndex];
expect(damageSection.lines).toEqual(expectedLines);
expect(wrapper.vm.sections.length).toBe(2);
});
describe('preferred shop section', () => {
test('has three lines', async () => {
test('has two lines', async () => {
// Arrange
const { wrapper } = getMountedComponent();
const preferredShopSectionIndex = 2;
const preferredShopSectionIndex = 0;
// Act
await tpaSubmit.beforeRouteEnter.call(
@ -390,15 +283,15 @@ describe('tpa-submit', () => {
// Assert
const preferredShopSection = wrapper.vm.sections[preferredShopSectionIndex];
expect(preferredShopSection.lines.length).toBe(3);
expect(preferredShopSection.lines.length).toBe(2);
});
test('first line is value returned from toTitleCase method', async () => {
test('title is value returned from toTitleCase method', async () => {
// Arrange
const initialData = { companyName: 'some value' };
const { wrapper } = getMountedComponent({}, initialData);
const expectedName = 'some expected name';
toTitleCase.mockImplementationOnce(() => expectedName);
const preferredShopSectionIndex = 2;
const preferredShopSectionIndex = 0;
// Act
await tpaSubmit.beforeRouteEnter.call(
@ -410,9 +303,9 @@ describe('tpa-submit', () => {
// Assert
const preferredShopSection = wrapper.vm.sections[preferredShopSectionIndex];
expect(preferredShopSection.lines[0]).toBe(expectedName);
expect(preferredShopSection.title).toBe(expectedName);
});
test('second line is expected and formatAddress called', async () => {
test('first line is expected and formatAddress called', async () => {
// Arrange
const address = {
streetAddress: '123 South Ln',
@ -430,7 +323,7 @@ describe('tpa-submit', () => {
const { wrapper } = getMountedComponent(initialStore);
const line = 'some returned line';
formatAddress.mockImplementationOnce(() => line);
const preferredShopSectionIndex = 2;
const preferredShopSectionIndex = 0;
// Act
await tpaSubmit.beforeRouteEnter.call(
@ -442,7 +335,7 @@ describe('tpa-submit', () => {
// Assert
const preferredShopSection = wrapper.vm.sections[preferredShopSectionIndex];
expect(preferredShopSection.lines[1]).toBe(line);
expect(preferredShopSection.lines[0]).toBe(line);
expect(formatAddress).toHaveBeenCalledTimes(1);
expect(formatAddress).toHaveBeenCalledWith(
address.streetAddress,
@ -452,7 +345,7 @@ describe('tpa-submit', () => {
address.zipCode
);
});
test('third line is expected and toDisplayPhoneNumber called', async () => {
test('second line is expected and toDisplayPhoneNumber called', async () => {
// Arrange
const phoneNumber = '9998887777';
const initialStore = {
@ -465,7 +358,7 @@ describe('tpa-submit', () => {
const { wrapper } = getMountedComponent(initialStore);
const expectedLine = 'returned from to display phone num';
toDisplayPhoneNumber.mockImplementationOnce(() => expectedLine);
const preferredShopSectionIndex = 2;
const preferredShopSectionIndex = 0;
// Act
await tpaSubmit.beforeRouteEnter.call(
@ -477,7 +370,7 @@ describe('tpa-submit', () => {
// Assert
const preferredShopSection = wrapper.vm.sections[preferredShopSectionIndex];
expect(preferredShopSection.lines[2]).toBe(expectedLine);
expect(preferredShopSection.lines[1]).toBe(expectedLine);
expect(toDisplayPhoneNumber).toHaveBeenCalledWith(phoneNumber);
});
});
@ -487,8 +380,14 @@ describe('tpa-submit', () => {
const lastName = 'Eddison';
const emailAddress = 'myname@gmail.com';
const servicePhone = '0001112222';
const providerCompanyName = 'Some Provider LLC';
const initialStore = {
order: {
serviceLocation: {
provider: {
companyName: providerCompanyName
}
},
contactInfo: {
firstName,
lastName,
@ -498,11 +397,11 @@ describe('tpa-submit', () => {
}
};
const { wrapper } = getMountedComponent(initialStore);
const expectedLine1 = 'Jones Eddison';
const expectedLine2 = emailAddress;
const expectedLine3 = 'some value returned';
toDisplayPhoneNumber.mockImplementation((number) => (number === servicePhone ? expectedLine3 : ''));
const contactInfoSectionIndex = 3;
const expectedEmail = emailAddress;
const expectedPhone = 'some value returned';
toDisplayPhoneNumber.mockImplementation((number) => (number === servicePhone ? expectedPhone : ''));
const contactInfoSectionIndex = 1;
wrapper.vm.getCmsContent.mockImplementation(() => '{custom:contactPhone} or {custom:contactEmail}');
// Act
await tpaSubmit.beforeRouteEnter.call(
@ -511,12 +410,59 @@ describe('tpa-submit', () => {
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.setSections();
// Assert
const contactInfoSection = wrapper.vm.sections[contactInfoSectionIndex];
expect(contactInfoSection.lines[0]).toBe(expectedLine1);
expect(contactInfoSection.lines[1]).toBe(expectedLine2);
expect(contactInfoSection.lines[2]).toBe(expectedLine3);
expect(contactInfoSection.lines[0]).toContain(expectedEmail);
expect(contactInfoSection.lines[0]).toContain(expectedPhone);
expect(toDisplayPhoneNumber).toHaveBeenCalledWith(servicePhone);
});
test('contact info section has expected content with extension', async () => {
// Arrange
const firstName = 'Jones';
const lastName = 'Eddison';
const emailAddress = 'myname@gmail.com';
const servicePhone = '0001112222';
const extension = '12345';
const providerCompanyName = 'Some Provider LLC';
const initialStore = {
order: {
serviceLocation: {
provider: {
companyName: providerCompanyName
}
},
contactInfo: {
firstName,
lastName,
emailAddress,
servicePhone,
extension
}
}
};
const { wrapper } = getMountedComponent(initialStore);
const expectedEmail = emailAddress;
const expectedPhone = 'some value returned';
toDisplayPhoneNumber.mockImplementation((number) => (number === servicePhone ? expectedPhone : ''));
const contactInfoSectionIndex = 1;
wrapper.vm.getCmsContent.mockImplementation(() => '{custom:contactPhone} or {custom:contactEmail}');
// Act
await tpaSubmit.beforeRouteEnter.call(
wrapper.vm,
{ query: { issPage: 'tpa-submit' } },
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.setSections();
// Assert
const contactInfoSection = wrapper.vm.sections[contactInfoSectionIndex];
expect(contactInfoSection.lines[0]).toContain(expectedEmail);
expect(contactInfoSection.lines[0]).toContain(expectedPhone);
expect(contactInfoSection.lines[0]).toContain(`Ext. ${extension}`);
expect(toDisplayPhoneNumber).toHaveBeenCalledWith(servicePhone);
});
});
@ -524,7 +470,6 @@ describe('tpa-submit', () => {
test.each([
['subHeaderTitle', 'SiteSubHeaderWidget', widgetFields.CONTENT_GROUP_WIDGET.HEADER_TEXT, 'site sub header'],
['subHeaderBodyOne', 'SiteSubHeaderWidget', widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT, 'sub header body one'],
['serviceSummaryText', 'ServiceSummaryContent', widgetFields.TEXT_BLOCK_WIDGET.TEXT, 'service summary text'],
['orderDetailsTitle', 'OrderDetailsContent', widgetFields.CONTENT_GROUP_WIDGET.HEADER_TEXT, 'order details title'],
['forwardButtonText', 'SiteFooterWidget', widgetFields.FOOTER_WIDGET.FORWARD_BUTTON_TEXT, 'forward button text']
])('computed %p returns expected value', (computedName, widgetLabel, fieldLabel, expected) => {

View file

@ -13,53 +13,52 @@
</div>
<div class="iss-heritage-container-width">
<div class="tpa-submit-container iss-heritage-content-container-width">
<!-- TODO fix styling -->
<alert
ref="alertIncomplete"
alertClass="alert-warning"
:cmsWidgetName="widget.alertIncomplete"
/>
<textBlock
ref="subHeaderTitle"
:customText="subHeaderTitle"
justifyText="center"
:marginTopSizeOverride="4"
class="text-color--black fs-5 tpa-submit__title--line-height" />
class="tpa-submit-title"/>
<textBlock
id="tpaSubmitSubHeaderBodyOne"
ref="tpaSubmitSubHeaderBodyOne"
:customText="subHeaderBodyOne"
class="small text-color--darker-gray" />
<textBlock
id="tpaSubmitSubHeaderBodyTwo"
ref="tpaSubmitSubHeaderBodyTwo"
:customText="subHeaderBodyTwo"
class="mb-4 small text-color--darker-gray" />
<buttonMain
ref="buttonMainOne"
variant="success"
:buttonText="forwardButtonText"
class="w-100 mb-5"
@clickEvent="forwardButtonAction" />
<hr class="mb-0" />
:customText="subHeaderBodyOne" />
<alert
v-if="recalibrationRequired"
ref="alertRecalWarning"
alertClass="alert-warning recal-alert"
:isCollapsible="true"
:cmsWidgetName="widget.alertRecalWarning"
@textLinkClicked="openRecalModal" />
<modal
ref="recalModal"
class="recal-modal"
:modalPosition="modalPositions.center"
:headerText="recalModalContent.headerText"
:footerButtonText="recalModalContent.footerButtonText"
@footerButtonEvent="closeRecalModal">
<div class="recal-modal-subheader">{{ recalModalContent.subHeaderText }}</div>
<img class="recal-modal-image" :src="recalModalContent.image" />
<div class="recal-modal-body" v-html="recalModalContent.bodyText"></div>
</modal>
<hr />
<div id="serviceSummarySection">
<textBlock
id="tpaSubmitServiceSummaryTitle"
ref="tpaSubmitServiceSummaryTitle"
:customText="serviceSummaryText"
:marginTopSizeOverride="4"
class="fw-bold fs-1 lh-lg text-color--black" />
<div class="px-4 my-3">
<div
v-for="(section, index) in sections"
:key="section.title">
<hr
v-if="index !== 0"
class="my-3" />
<reviewBlock
:id="'review-block-' + index"
:customHeaderText="section.title"
:lines="section.lines"
@clickEdit="() => section.onClickEdit()" />
</div>
<div
v-for="(section, index) in sections"
:key="section.title">
<hr v-if="index !== 0" />
<reviewBlock
:id="'review-block-' + index"
:customHeaderText="section.title"
:lines="section.lines"
:editLinkText="section.editLinkText"
@clickEdit="() => section.onClickEdit()" />
</div>
</div>
<hr class="mb-0" />
<hr />
<div
id="submitOrderDetailsSection"
ref="submitOrderDetailsSection">
@ -67,17 +66,10 @@
id="tpaSubmitOrderDetailsTitle"
ref="tpaSubmitOrderDetailsTitle"
:customText="orderDetailsTitle"
:marginTopSizeOverride="4"
class="fw-bold text-color--black" />
<textBlock
id="tpaSubmitOrderDetailsBody"
ref="tpaSubmitOrderDetailsBody"
:customText="orderDetailsBody"
:marginTopSizeOverride="4"
class="mb-4 px-4 small text-color--darker-gray" />
:marginTopSizeOverride="0"
class="fw-bold order-details-title" />
<deductibleBox
ref="deductibleBox"
class="px-3"
:value="deductibleBoxValue" />
</div>
<siteFooter
@ -104,6 +96,8 @@ import reviewBlock from '@/layouts/tpa-submit/review-block/review-block.vue';
import deductibleBox from '@/layouts/tpa-submit/deductible-box/deductible-box.vue';
import contactDetailsDrawer from '@/layouts/tpa-submit/contact-details-drawer/contact-details-drawer.vue';
import siteFooter from '@/iss-components/site-footer/site-footer.vue';
import alert from '@/ux-components/alert/alert.vue';
import modal from '@/digital-components/modal/modal.vue';
// Supporting files
import {
@ -114,21 +108,16 @@ import {
import { Form } from 'vee-validate';
import BaseFormMixin from '@/mixins/base-form-mixin.js';
import widgetFields from '@/constants/cms-widget-fields.js';
import { useMainStore } from '@/store';
import {
toTitleCase,
toDisplayPhoneNumber,
formatAddress,
formatAmountInDollars
} from '@/helpers/text-helper.js';
import {
getDamageDisplayContent,
getLocationAnswer
} from '@/helpers/damage-review-content-generator.js';
import damageLocationsSelected from '@/constants/damage-locations-selected.js';
import { submitWorkOrder } from '@/helpers/order-helper.js';
import bailoutMessage from '@/constants/bailoutMessage';
import submitType from '@/constants/submit-type';
import { modalPositions } from '@/constants/component-variants';
const VERIFYING_COVERAGE = 'Verifying coverage';
@ -143,7 +132,9 @@ export default {
siteFooter,
contactDetailsDrawer,
// eslint-disable-next-line vue/no-reserved-component-names
Form
Form,
alert,
modal
},
mixins: [BaseFormMixin],
async beforeRouteEnter(to, from, next) {
@ -154,27 +145,19 @@ export default {
});
},
data() {
const { companyName } = useMainStore().order.serviceLocation.provider;
return {
sections: [],
widget: {
siteHeader: 'SiteHeaderWidget',
siteSubHeader: 'SiteSubHeaderWidget',
serviceSummary: 'ServiceSummaryContent',
subheader: {
vehicle: 'VehicleSubTitle',
damage: 'DamageSubTitle',
shop: 'PreferredShopSubTitle',
contactInfo: 'ContactDetailsSubTitle'
},
damageLocations: 'DamageLocationsWidget',
orderDetails: 'OrderDetailsContent',
footer: 'SiteFooterWidget'
footer: 'SiteFooterWidget',
alertIncomplete: 'AlertIncompleteWidget',
alertRecalWarning: 'AlertRecalWarningWidget',
editShopLinkText: 'EditShopLinkTextWidget',
contactDetails: 'ContactDetailsSectionWidget'
},
companyName,
customValueMap: {
glassShop: companyName
}
modalPositions
};
},
computed: {
@ -197,12 +180,6 @@ export default {
);
return getStringWithCustomValues(cmsContent, this.customValueMap);
},
serviceSummaryText() {
return this.getCmsContent(
this.widget.serviceSummary,
widgetFields.TEXT_BLOCK_WIDGET.TEXT
);
},
orderDetailsTitle() {
return this.getCmsContent(
this.widget.orderDetails,
@ -227,53 +204,21 @@ export default {
);
},
isVerified() {
return useMainStore().isVerified;
return this.mainStore.isVerified;
},
currentDeductible() {
return useMainStore().order.currentDeductible;
return this.mainStore.order.currentDeductible;
},
deductibleBoxValue() {
return this.isVerified
? formatAmountInDollars(this.currentDeductible)
: VERIFYING_COVERAGE;
},
getVehicleLines() {
const { year, make, model } = useMainStore().order.vehicle;
const line = [year, make, model]
.filter((v) => v != null && v !== '')
.join(' ');
return [line];
},
locationAnswers() {
return this.getInputQuestionWidgetAnswersNullSafe(this.widget.damageLocations);
},
driverSideDamageAnswers() {
const answerContent = getLocationAnswer(
damageLocationsSelected.DRIVER,
this.locationAnswers
);
return this.getInputQuestionWidgetAnswersNullSafe(answerContent?.SubWidgetName);
},
passengerSideDamageAnswers() {
const answerContent = getLocationAnswer(
damageLocationsSelected.PASSENGER,
this.locationAnswers
);
return this.getInputQuestionWidgetAnswersNullSafe(answerContent?.SubWidgetName);
},
getDamageLines() {
const { glassToReplace, isRepair } = useMainStore().order.damage;
return getDamageDisplayContent(
this.locationAnswers,
this.driverSideDamageAnswers,
this.passengerSideDamageAnswers,
glassToReplace,
isRepair
);
getPreferredShopTitle() {
return toTitleCase(this.mainStore.order.serviceLocation.provider.companyName ?? '');
},
getPreferredShopLines() {
const { phoneNumber, address } =
useMainStore().order.serviceLocation.provider;
const { phoneNumber, address } = this.mainStore.order.serviceLocation.provider;
const { streetAddress, city, state, zipCode } = address;
const displayAddress = formatAddress(
streetAddress,
@ -284,58 +229,86 @@ export default {
);
const displayPhoneNumber = toDisplayPhoneNumber(phoneNumber);
return [
toTitleCase(this.companyName ?? ''),
displayAddress,
displayPhoneNumber
];
},
getEditShopLinkText() {
return this.getCmsContent(
this.widget.editShopLinkText,
widgetFields.TEXT_BLOCK_WIDGET.TEXT
);
},
getContactInfoTitle() {
const cmsContent = this.getCmsContent(
this.widget.contactDetails,
widgetFields.CONTENT_GROUP_WIDGET.HEADER_TEXT
);
return getStringWithCustomValues(cmsContent, this.customValueMap);
},
getContactInfoLines() {
const { firstName, lastName, emailAddress, servicePhone } =
useMainStore().contactInfo;
const cmsContent = this.getCmsContent(
this.widget.contactDetails,
widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT
);
return [
`${firstName} ${lastName}`,
emailAddress ?? '',
toDisplayPhoneNumber(servicePhone)
getStringWithCustomValues(cmsContent, this.customValueMap)
];
},
getEditContactInfoLinkText() {
return this.getCmsContent(
this.widget.contactDetails,
widgetFields.CONTENT_GROUP_WIDGET.FOOTER_TEXT
);
},
recalModalContent() {
const widgetName = 'RecalModal';
return {
headerText: this.getCmsContent(widgetName, widgetFields.CONTENT_GROUP_WIDGET.HEADER_TEXT),
subHeaderText: this.getCmsContent(widgetName, widgetFields.CONTENT_GROUP_WIDGET.SUBHEADER_TEXT),
bodyText: this.getCmsContent(widgetName, widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT),
footerButtonText: this.getCmsContent(widgetName, widgetFields.CONTENT_GROUP_WIDGET.FOOTER_TEXT),
image: this.getCmsContent(widgetName, widgetFields.CONTENT_GROUP_WIDGET.IMAGE)
}
},
recalibrationRequired() {
return this.mainStore.hasRecalibrationPart;
},
customValueMap() {
let contactPhone = toDisplayPhoneNumber(this.mainStore.contactInfo.servicePhone);
if (this.mainStore.contactInfo.extension) {
contactPhone += ` Ext. ${this.mainStore.contactInfo.extension}`;
}
return {
glassShop: this.getPreferredShopTitle,
contactPhone: contactPhone,
contactEmail: this.mainStore.contactInfo.emailAddress ?? ''
}
}
},
methods: {
setSections() {
const vehicleScenario = useMainStore().isPolicyVehicle
? this.navigationScenarios.EDIT_POLICY_VEHICLE
: this.navigationScenarios.EDIT_VEHICLE;
this.sections = [
this.getSection(
this.widget.subheader.vehicle,
this.getVehicleLines,
() => this.navigate(vehicleScenario)
),
// eslint-disable-next-line max-len
this.getSection(
this.widget.subheader.damage,
this.getDamageLines,
() => this.navigate(this.navigationScenarios.EDIT_DAMAGE)
),
this.getSection(
this.widget.subheader.shop,
this.getPreferredShopTitle,
this.getPreferredShopLines,
() => this.navigate(this.navigationScenarios.EDIT_PREFERRED_SHOP)
this.getEditShopLinkText,
() => this.navigate(this.navigationScenarios.EDIT_PREFERRED_SHOP),
),
// eslint-disable-next-line max-len
this.getSection(
this.widget.subheader.contactInfo,
this.getContactInfoTitle,
this.getContactInfoLines,
this.getEditContactInfoLinkText,
this.openContactDetailsModal
)
];
},
getSection(widgetName, lines, onClick) {
getSection(title, lines, editLinkText, onClick) {
return {
title: this.getCmsContent(
widgetName,
widgetFields.TEXT_BLOCK_WIDGET.TEXT
),
title: title,
lines,
editLinkText,
onClickEdit: onClick
};
},
@ -344,7 +317,7 @@ export default {
await submitWorkOrder({ submitType: submitType.TPA }).then(() => {
this.navigate(this.navigationScenarios.CLICKED_FORWARD);
}).catch((submitError) => {
useMainStore().setBailout(bailoutMessage.saveSessionError(submitError.data));
this.mainStore.setBailout(bailoutMessage.saveSessionError(submitError.data));
this.navigate(
this.navigationScenarios.SAVE_SESSION_FAILED,
this.$route,
@ -372,6 +345,12 @@ export default {
},
openContactDetailsModal() {
this.$refs.contactDetailsDrawer.openModal();
},
openRecalModal() {
this.$refs.recalModal.openModal();
},
closeRecalModal() {
this.$refs.recalModal.closeModal();
}
}
};
@ -387,26 +366,69 @@ export default {
}
.tpa-submit {
#tpaSubmitOrderDetailsBody {
p {
font-size: .875rem;
}
}
.tpa-submit__title--line-height {
line-height: map-get($spacers, 6);
.alert-warning {
margin-top: 1.25rem;
margin-bottom: 0rem;
}
.text-color--darker-gray {
color: $darker-gray;
.tpa-submit-title {
font-size: 1.25rem;
color: $black;
font-weight: 400;
margin-top: 1.25rem;
margin-bottom: .625rem;
}
.text-color--black {
.recal-alert {
margin-bottom: 4rem;
}
b {
font-weight: $font-weight-bold;
color: $black;
}
hr {
opacity: 1;
color: $gray-350;
margin: 1.875rem 0rem;
}
.fw-bold {
color: $black;
}
.order-details-title {
margin-bottom: .625rem;
}
.modal.recal-modal .modal-dialog {
.modal-header {
justify-content: start;
margin: 0rem;
padding: 1.25rem 1.25rem .625rem 1.25rem;
.modal-title {
font-weight: $font-weight-bolder;
font-size: .875rem;
line-height: 1.25rem;
text-align: start;
justify-content: start;
}
.btn-close {
display: none;
}
}
.modal-body {
padding: 0rem 1.25rem 1.25rem 1.25rem;
b {
font-weight: 600;
}
}
.recal-modal-subheader {
font-weight: $font-weight-bold;
color: $black;
}
.recal-modal-image {
margin: 1.25rem 0rem
}
}
}
</style>

View file

@ -310,7 +310,14 @@ export default {
this.mainStore.updatePolicyData(this.welcomePageModel);
const promises = [];
promises.push(this.configureZip().then(async () => await this.mainStore.getBillToInfo()));
if (!this.mainStore.order.loadedFromCookie) {
// Clear duplicate orders if navigating away from the welcome page after visiting duplicate check page.
if (this.mainStore.order.visitedDuplicateCheckPage) {
this.mainStore.clearDuplicateOrders();
}
// Skip duplicate check if loaded from cookie or already visited duplicate check page.
if (!this.mainStore.order.loadedFromCookie && !this.mainStore.order.visitedDuplicateCheckPage) {
promises.push(this.mainStore.getDuplicateReferrals());
}
promises.push(this.mainStore.getCoveragePolicyInfo());
@ -472,6 +479,7 @@ export default {
</script>
<style lang="scss" scoped>
@import '@/styles/ux-variables-svg-strings.scss';
form {
.container-fluid {
display: flex;
@ -489,6 +497,22 @@ form {
.form-group {
margin-top: 1.25rem;
:deep(.input-wrapper) {
input[type="date"] {
max-height: 3rem;
}
}
&.has-error {
:deep(.input-wrapper) {
input[type="date"]::-webkit-calendar-picker-indicator {
background-color: $svg-calendar-error-fill-color;
// eslint-disable-next-line max-len
background-image: url($svg-error-calendar-graphic);
}
}
}
}
}
}

View file

@ -27,6 +27,8 @@ const navigationScenarios = Object.freeze({
// Policy Holder Details
CLICKED_FORWARD_POLICY_VERIFIED: 'CLICKED_FORWARD_POLICY_VERIFIED',
CLICKED_BACK_POLICY_UNVERIFIED: 'CLICKED_BACK_POLICY_UNVERIFIED',
CLICKED_BACK_POLICY_VERIFIED: 'CLICKED_BACK_POLICY_VERIFIED',
// Policy Vehicle
CLICKED_FORWARD_LISTED_VEHICLE: 'CLICKED_FORWARD_LISTED_VEHICLE',

View file

@ -9,11 +9,11 @@ const routingTable = () => [
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationIssPageValue: issPageValues.POLICY_HOLDER_DETAILS
destinationIssPageValue: issPageValues.WELCOME_PAGE
},
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationIssPageValue: issPageValues.VEHICLE_DAMAGE
destinationIssPageValue: issPageValues.POLICY_HOLDER_DETAILS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_BAILOUT,
@ -26,7 +26,7 @@ const routingTable = () => [
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationIssPageValue: issPageValues.VEHICLE_SELECTION
destinationIssPageValue: issPageValues.POLICY_HOLDER_DETAILS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_REPAIR,
@ -386,7 +386,7 @@ const routingTable = () => [
},
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationIssPageValue: issPageValues.POLICY_HOLDER_DETAILS
destinationIssPageValue: issPageValues.VEHICLE_SELECTION
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_DUPLICATES,
@ -394,7 +394,7 @@ const routingTable = () => [
},
{
scenario: navigationScenarios.CLICKED_FORWARD_POLICY_UNVERIFIED,
destinationIssPageValue: issPageValues.POLICY_HOLDER_DETAILS
destinationIssPageValue: issPageValues.VEHICLE_SELECTION
},
{
scenario: navigationScenarios.CLICKED_FORWARD_POLICY_VERIFIED_NO_VEHICLES,
@ -423,7 +423,7 @@ const routingTable = () => [
},
{
scenario: navigationScenarios.CLICKED_FORWARD_POLICY_UNVERIFIED,
destinationIssPageValue: issPageValues.POLICY_HOLDER_DETAILS
destinationIssPageValue: issPageValues.VEHICLE_SELECTION
},
{
scenario: navigationScenarios.CLICKED_FORWARD_POLICY_VERIFIED_NO_VEHICLES,
@ -435,7 +435,7 @@ const routingTable = () => [
},
{
scenario: navigationScenarios.CLICKED_FORWARD_LOADED_DUPLICATE_WITH_POLICY_VEHICLE,
destinationIssPageValue: issPageValues.VEHICLE_DAMAGE
destinationIssPageValue: issPageValues.POLICY_HOLDER_DETAILS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_LOADED_DUPLICATE_WITH_NON_POLICY_VEHICLE,
@ -451,16 +451,20 @@ const routingTable = () => [
issPageValue: issPageValues.POLICY_HOLDER_DETAILS,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
scenario: navigationScenarios.CLICKED_BACK_POLICY_UNVERIFIED,
destinationIssPageValue: issPageValues.VEHICLE_SELECTION
},
{
scenario: navigationScenarios.CLICKED_BACK_POLICY_VERIFIED,
destinationIssPageValue: issPageValues.WELCOME_PAGE
},
{
scenario: navigationScenarios.CLICKED_FORWARD_POLICY_UNVERIFIED,
destinationIssPageValue: issPageValues.VEHICLE_SELECTION
destinationIssPageValue: issPageValues.VEHICLE_DAMAGE
},
{
scenario: navigationScenarios.CLICKED_FORWARD_POLICY_VERIFIED,
destinationIssPageValue: issPageValues.POLICY_VEHICLES
destinationIssPageValue: issPageValues.VEHICLE_DAMAGE
}
]
},
@ -473,7 +477,7 @@ const routingTable = () => [
},
{
scenario: navigationScenarios.CLICKED_FORWARD_LISTED_VEHICLE,
destinationIssPageValue: issPageValues.VEHICLE_DAMAGE
destinationIssPageValue: issPageValues.POLICY_HOLDER_DETAILS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_NON_LISTED_VEHICLE,
@ -776,11 +780,11 @@ const routingTable = () => [
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationIssPageValue: issPageValues.POLICY_VEHICLES
destinationIssPageValue: issPageValues.WELCOME_PAGE
},
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationIssPageValue: issPageValues.VEHICLE_DAMAGE
destinationIssPageValue: issPageValues.POLICY_HOLDER_DETAILS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_CAR_ID_NOT_FOUND,

View file

@ -19,7 +19,7 @@ describe('Router', () => {
router.push = jest.fn();
router.navigate(scenario, currentRoute);
expect(router.push.mock.calls[0][0].query.issPage).toBe(issPageValues.POLICY_HOLDER_DETAILS);
expect(router.push.mock.calls[0][0].query.issPage).toBe(issPageValues.VEHICLE_SELECTION);
});
it('Should set state as expected', () => {

View file

@ -197,6 +197,7 @@ export const getDefaultState = () => ({
homePhone: null,
alternativePhone: null,
servicePhone: null,
extension: null,
requestTextUpdates: false,
notesForTechnician: ''
},
@ -225,7 +226,8 @@ export const getDefaultState = () => ({
loadedFromCookie: false,
loadedFromDupeCheck: null,
loadedSessionClearedPreviousData: null,
availableVaps: null
availableVaps: null,
visitedDuplicateCheckPage: false
},
applicationUser: {
experiments: [],
@ -354,6 +356,7 @@ export const useMainStore = defineStore({
homePhone: s.order.contactInfo.homePhone,
alternativePhone: s.order.contactInfo.alternativePhone,
servicePhone: s.order.contactInfo.servicePhone,
extension: s.order.contactInfo.extension,
requestTextUpdates: s.order.contactInfo.requestTextUpdates ?? false,
notesForTechnician: s.order.contactInfo.notesForTechnician
}),
@ -595,6 +598,12 @@ export const useMainStore = defineStore({
return Promise.reject(e);
}
},
clearDuplicateOrders() {
this.applicationUser.duplicateOrders = [];
},
updateDuplicateCheckVisited(visited) {
this.order.visitedDuplicateCheckPage = visited;
},
updateCoverageStatus(status) {
this.order.insuranceCoverage.coverageStatus = status;
},
@ -1012,14 +1021,13 @@ export const useMainStore = defineStore({
});
},
getProviders(serviceZipCode) {
getProviders(serviceZipCode, shopRadiusInMiles = 100) {
const { carId, isBigTruck } = this.order.vehicle;
const damageType = this.damage.isRepair ? 'Repair' : 'Replace';
const { parentAccountNumber } = this.order;
const partsWithRecal = getTopLevelGlassPartsWithRecal(this.order.lineItems.glassParts);
const windshieldPartWithRecal = partsWithRecal?.length > 0 ? partsWithRecal[0] : null;
const safeliteOnly = true;
const shopRadiusInMiles = 100;
let url = `${endpoints.GetProviders.url}/${serviceZipCode}/${damageType}/${shopRadiusInMiles}/${parentAccountNumber}/${safeliteOnly}/${carId}`;
if (windshieldPartWithRecal) {
@ -1456,7 +1464,7 @@ export const useMainStore = defineStore({
emailAddress: contactInfo.emailAddress || customer.emailAddress,
firstName: contactInfo.firstName || customer.firstName,
lastName: contactInfo.lastName || customer.lastName,
homePhone: contactInfo.homePhone,
homePhone: contactInfo.extension && contactInfo.homePhone ? contactInfo.homePhone + contactInfo.extension : contactInfo.homePhone,
servicePhone: contactInfo.servicePhone,
alternativePhone: contactInfo.alternativePhone,
isSmsOptIn: contactInfo.requestTextUpdates ?? false
@ -1590,7 +1598,8 @@ export const useMainStore = defineStore({
order.contactInfo.firstName = data?.customer?.firstName;
order.contactInfo.lastName = data?.customer?.lastName;
order.contactInfo.emailAddress = data?.customer?.emailAddress;
order.contactInfo.homePhone = data?.customer?.homePhone;
order.contactInfo.extension = data?.customer?.homePhone?.slice(10);
order.contactInfo.homePhone = data?.customer?.homePhone?.slice(0, 10);
order.contactInfo.servicePhone = data?.customer?.servicePhone;
order.contactInfo.alternativePhone = data?.customer?.alternativePhone;
order.contactInfo.requestTextUpdates = data?.customer?.isSmsOptIn;
@ -1658,7 +1667,8 @@ export const useMainStore = defineStore({
order.contactInfo.firstName = data?.customer?.firstName;
order.contactInfo.lastName = data?.customer?.lastName;
order.contactInfo.emailAddress = data?.customer?.emailAddress;
order.contactInfo.homePhone = data?.customer?.homePhone;
order.contactInfo.extension = data?.customer?.homePhone?.slice(10);
order.contactInfo.homePhone = data?.customer?.homePhone?.slice(0, 10);
order.contactInfo.servicePhone = data?.customer?.servicePhone;
order.contactInfo.alternativePhone = data?.customer?.alternativePhone;
order.contactInfo.requestTextUpdates = data?.customer?.isSmsOptIn;
@ -1720,6 +1730,7 @@ export const useMainStore = defineStore({
order.eon = data?.eon;
order.workOrderId = data?.workOrderId;
order.loadedFromCookie = true;
order.visitedDuplicateCheckPage = true;
order.loadedSessionClearedPreviousData = false;
issConfig.enableContinueFromCookie = false;
this.updateIsSafeliteProvider(data?.provider?.isSafeliteProvider);
@ -2042,6 +2053,7 @@ export const useMainStore = defineStore({
this.order.carrierPhoneNumber = null;
this.order.customerPortalLoginToken = null;
this.order.loadedFromCookie = false;
this.order.visitedDuplicateCheckPage = false;
},
resetPolicy() {
@ -2096,6 +2108,7 @@ export const useMainStore = defineStore({
this.order.contactInfo.homePhone = null;
this.order.contactInfo.alternativePhone = null;
this.order.contactInfo.servicePhone = null;
this.order.contactInfo.extension = null;
this.order.contactInfo.requestTextUpdates = false;
this.order.contactInfo.notesForTechnician = '';
},
@ -2291,6 +2304,7 @@ export const useMainStore = defineStore({
this.order.customer.address.zipCode = customerQuestions.addressQuestions.zipCode;
this.order.customer.firstName = customerQuestions.firstName;
this.order.customer.lastName = customerQuestions.lastName;
this.order.customer.emailAddress = customerQuestions.email;
this.order.serviceLocation.zipCode = customerQuestions.addressQuestions.zipCode;
},
updateIsSafeliteProvider(isSafelite) {
@ -2576,6 +2590,7 @@ export const useMainStore = defineStore({
contact.homePhone = phoneNumbers.home !== undefined ? phoneNumbers.home : contact.homePhone;
contact.alternativePhone = phoneNumbers.alternative !== undefined ? phoneNumbers.alternative : contact.alternativePhone;
contact.servicePhone = phoneNumbers.service !== undefined ? phoneNumbers.service : contact.servicePhone;
contact.extension = phoneNumbers.extension !== undefined ? phoneNumbers.extension : contact.extension;
},
GetExperimentsByUser(userId) {

View file

@ -494,18 +494,21 @@ describe('Store', () => {
const homePhone = getRandomInt(1000000000, 9999999999);
const servicePhone = getRandomInt(1000000000, 9999999999);
const altPhone = getRandomInt(1000000000, 9999999999);
const extension = getRandomInt(10000, 99999);
// Act
store.updatePhoneNumbers({
home: homePhone,
service: servicePhone,
alternative: altPhone
alternative: altPhone,
extension: extension
});
// Assert
expect(store.contactInfo.homePhone).toEqual(homePhone);
expect(store.contactInfo.servicePhone).toEqual(servicePhone);
expect(store.contactInfo.alternativePhone).toEqual(altPhone);
expect(store.contactInfo.extension).toEqual(extension);
});
it('All null values => contact info set in store to all nulls', () => {
// Act

View file

@ -9,7 +9,7 @@ body {
font-size: 16px;
letter-spacing: $letter-spacing-primary;
background-color: #fff;
color: #4d5151;
color: $body-color;
.container-fluid {
.prevent-squish {
@ -179,6 +179,10 @@ body {
}
}
}
.site-sub-header-container {
margin-top: 1.25rem;
}
}
:root {

View file

@ -115,21 +115,3 @@ caption,
font-weight: 800;
font-style: normal;
}
@font-face {
font-family: "Urbanist";
src:
url("@/assets/fonts/Urbanist-MediumItalic.woff2") format("woff2"),
url("@/assets/fonts/Urbanist-MediumItalic.woff") format("woff");
font-weight: 500;
font-style: italic;
}
@font-face {
font-family: "Urbanist";
src:
url("@/assets/fonts/Urbanist-BoldItalic.woff2") format("woff2"),
url("@/assets/fonts/Urbanist-BoldItalic.woff") format("woff");
font-weight: 700;
font-style: italic;
}

View file

@ -14,17 +14,17 @@ html {
border-radius: 60px;
.button-content {
border: none;
}
&:hover {
@include box-shadow-hover($red-200);
z-index: 5;
border: 1px solid $red;
}
input[type="radio"]:focus+.list-button-content {
box-shadow: 0 0 0 2.5px $red;
}
&:not(.selected):not(.no-hover):hover {
position: relative;
@include box-shadow-hover($red-200);
z-index: 5;
}
}
&.list-card {

View file

@ -1,7 +1,9 @@
.base-input-button {
&:not(.has-error):hover {
&:not(.disabled) {
cursor: pointer;
}
&:not(.has-error):hover {
&.list-button,
&.list-button-horizontal,
&.list-card {

View file

@ -1,4 +1,5 @@
$svg-calendar-picker: "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";
$svg-error-calendar-graphic: "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='" + $svg-calendar-error-stroke-color + "'/%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='" + $svg-calendar-error-stroke-color + "'/%3E%3C/svg%3E";
$svg-date-picker-nav-back-button: "data:image/svg+xml,%3Csvg width='7' height='12' viewBox='0 0 7 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0.331685 6.00121C0.330445 5.82446 0.399256 5.65442 0.523053 5.52832L5.84499 0.198256C5.97188 0.0713149 6.14397 -8.63821e-08 6.32341 -6.72174e-08C6.50285 -4.80527e-08 6.67495 0.071315 6.80183 0.198256C6.92872 0.325198 7 0.497368 7 0.67689C7 0.856412 6.92872 1.02858 6.80183 1.15552L1.94874 6.00121L6.80183 10.8526C6.92745 10.9796 6.99751 11.1512 6.99662 11.3299C6.99572 11.5085 6.92393 11.6794 6.79705 11.8051C6.67016 11.9308 6.49857 12.0009 6.32003 12C6.14148 11.9991 5.97061 11.9273 5.84499 11.8003L0.526881 6.47601C0.401568 6.34983 0.331375 6.17908 0.331685 6.00121Z' fill='%231574A1'/%3E%3C/svg%3E%0A";
$svg-date-picker-forward-button: "data:image/svg+xml,%3Csvg width='7' height='12' viewBox='0 0 7 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.66831 5.99879C6.66955 6.17554 6.60074 6.34558 6.47695 6.47168L1.15501 11.8017C1.02812 11.9287 0.85603 12 0.676587 12C0.497145 12 0.325053 11.9287 0.198168 11.8017C0.0712831 11.6748 7.268e-09 11.5026 8.07183e-09 11.3231C8.87567e-09 11.1436 0.0712831 10.9714 0.198168 10.8445L5.05126 5.99879L0.198168 1.14736C0.0725521 1.02042 0.00248585 0.848755 0.00338306 0.670131C0.00428028 0.491506 0.0760674 0.320554 0.202952 0.194881C0.329837 0.0692091 0.501426 -0.000888818 0.679971 9.54485e-06C0.858515 0.000906955 1.02939 0.0727263 1.15501 0.199668L6.47312 5.52399C6.59843 5.65017 6.66862 5.82092 6.66831 5.99879Z' fill='%231574A1'/%3E%3C/svg%3E%0A";
$svg-drop-off-alert: "data:image/svg+xml,%3Csvg viewBox='0 0 12 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23clip0_13957_112512)'%3E%3Cpath d='M5.99865 0C4.81147 4.82643e-07 3.65095 0.352111 2.66392 1.01179C1.67688 1.67146 0.907678 2.60907 0.45361 3.70599C-0.000459241 4.80291 -0.11899 6.00986 0.113013 7.17415C0.345015 8.33845 0.917126 9.40778 1.75697 10.2469C2.59682 11.086 3.66666 11.6571 4.83117 11.8881C5.99567 12.119 7.20251 11.9994 8.29902 11.5443C9.39553 11.0893 10.3324 10.3192 10.9912 9.33159C11.65 8.34396 12.0011 7.18313 12 5.99594C11.9971 4.40566 11.3638 2.88142 10.2388 1.75742C9.11375 0.633431 7.58894 0.00143011 5.99865 0V0ZM5.99865 11.2478C4.96135 11.2473 3.94748 10.9392 3.08518 10.3627C2.22288 9.7861 1.55085 8.96685 1.15401 8.00846C0.75718 7.05006 0.653353 5.99554 0.855656 4.97815C1.05796 3.96077 1.55731 3.02618 2.29061 2.29251C3.0239 1.55884 3.95823 1.059 4.97551 0.856176C5.99279 0.653349 7.04737 0.756633 8.00597 1.15297C8.96457 1.54931 9.78416 2.22092 10.3612 3.08293C10.9382 3.94493 11.2467 4.95864 11.2478 5.99594C11.2478 7.38835 10.6949 8.72377 9.71053 9.70861C8.7262 10.6934 7.39106 11.2471 5.99865 11.2478V11.2478Z' fill='%2306577C'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M6.22736 8.84695C6.30613 8.76818 6.35038 8.66135 6.35038 8.54996V5.30996C6.35038 5.19857 6.30613 5.09174 6.22736 5.01298C6.1486 4.93421 6.04177 4.88996 5.93038 4.88996C5.81899 4.88996 5.71216 4.93421 5.63339 5.01298C5.55463 5.09174 5.51038 5.19857 5.51038 5.30996V8.54996C5.51038 8.66135 5.55463 8.76818 5.63339 8.84695C5.71216 8.92571 5.81899 8.96996 5.93038 8.96996C6.04177 8.96996 6.1486 8.92571 6.22736 8.84695ZM5.69704 3.97918C5.76611 4.02533 5.84731 4.04996 5.93038 4.04996C5.98558 4.05012 6.04026 4.03936 6.09129 4.01831C6.14232 3.99726 6.18868 3.96633 6.22771 3.9273C6.26675 3.88827 6.29768 3.8419 6.31873 3.79088C6.33978 3.73985 6.35053 3.68516 6.35038 3.62996C6.35038 3.54689 6.32574 3.46569 6.27959 3.39662C6.23344 3.32755 6.16785 3.27372 6.0911 3.24193C6.01436 3.21014 5.92991 3.20183 5.84844 3.21803C5.76697 3.23424 5.69213 3.27424 5.63339 3.33298C5.57465 3.39171 5.53465 3.46655 5.51845 3.54802C5.50224 3.6295 5.51056 3.71394 5.54235 3.79069C5.57414 3.86743 5.62797 3.93303 5.69704 3.97918Z' fill='%2306577C'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0_13957_112512'%3E%3Crect width='12' height='12' fill='white'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E%0A";

View file

@ -70,8 +70,13 @@ $orange: #fd7e14;
$teal: #20c997;
$cyan: #0dcaf0;
//Heritage
$heritage-blue-primary: #0070D1;
$heritage-blue-secondary: #167cac;
// Darker gray
$darker-gray: #525656;
$lighter-gray: #9aa1a3;
// scss-docs-start colors-map
$colors: (
@ -114,7 +119,7 @@ $theme-colors: (
);
//Default font color
$body-color: $gray-600;
$body-color: $darker-gray;
//Fonts
$font-family-sans-serif: Urbanist, Roboto, Arial, sans-serif;
@ -153,6 +158,7 @@ $font-weight-bolder: bolder;
$border-radius: 0.25rem;
$border-radius-sm: 0.2rem;
$border-radius-lg: 0.5rem; //Used for buttons. Can be used for other things, of course.
$border-radius-xl: 1.375rem;
$border-radius-pill: 50rem;
$border-radius-list-button: 1.375rem; // Used for list buttons
@ -205,6 +211,11 @@ $alert-bg-scale: -90%;
$alert-border-scale: -100%;
$alert-color-scale: 40%;
$alert-yellow-bg: #fef6e8;
$alert-yellow-color: #e86421;
$alert-red-bg: #fbe9e8;
$alert-red-color: $red;
//Modal animation
// This affects all [Bootstrap] modals
$modal-fade-transform: translate(0, 100%);
@ -216,9 +227,8 @@ $enable-important-utilities: false;
// Letter Spacing
$letter-spacing-primary: 0.03em;
// Borders
$border-input: 1px solid rgba(179, 180, 181);
$border-input-focus: 2.5px solid $blue;
$border-input: 1px solid $gray-350;
$border-input-focus: 2.5px solid $heritage-blue-primary;
//Heritage
$heritage-blue-primary: #0070D1;
$heritage-blue-secondary: #167cac;
$svg-calendar-error-fill-color: $alert-red-bg;
$svg-calendar-error-stroke-color: '%23db0020'; // URL encoded #db0020

View file

@ -1,64 +1,85 @@
<template>
<div
class="alert fade show text-center mb-0 py-2 px-4"
class="alert fade show"
role="alert"
:class="[
isDismissible ? 'alert-dismissible' : '',
alertClass,
cssClassNameForCmsWidget,
]">
<p class="m-0 fw-bold alert-heading">
{{ alertHeadline }}
</p>
<template
v-for="paragraph in splitAlertCopyForParagraphTag"
:key="paragraph">
<p
v-if="!doesCopyContainRouterLink(paragraph) && !doesCopyContainTextLink(paragraph)"
class="m-0 text-body small"
v-html="paragraph"></p>
<p
v-else
class="m-0 text-body small">
<template v-if="doesCopyContainRouterLink(paragraph)">
<textBlock
:customText="paragraph"
class="mb-1"
marginTopSizeOverride="0" />
</template>
<template
v-for="copy in splitCopyOnCMSPlaceHolder(paragraph)"
v-else
:key="copy">
<span v-if="doesCopyContainTextLink(copy)">
<textLink
linkType="text"
:text="getRouterLinkDisplayTextFromCopy(copy)"
href="#!"
:data-bs-target="'#' + getRouterLinkRouteFromCopy(copy)"
@click-event="
$emit('textLinkClicked', getRouterLinkRouteFromCopy(copy))
" />
</span>
<span
v-else
v-html="copy"></span>
</template>
</p>
</template>
<button
type="button"
class="btn-close p-2"
data-bs-dismiss="alert"
aria-label="Close">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 23.7 23.7"
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" />
<div class="alert-header">
<svg class="alert-icon" xmlns="http://www.w3.org/2000/svg" width="20" height="21" viewBox="0 0 20 21">
<path fill-rule="nonzero" d="M10 .5c5.523 0 10 4.477 10 10s-4.477 10-10 10-10-4.477-10-10S4.477.5 10 .5zm.043 13.6a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm.77-9.2h-1.54l-.088.009a.502.502 0 0 0-.299.193.66.66 0 0 0-.125.47l.747 6.806.017.096c.065.249.263.425.495.426l.084-.008c.22-.042.396-.246.427-.51l.793-6.805.004-.102a.651.651 0 0 0-.127-.37.489.489 0 0 0-.388-.205z"/>
</svg>
</button>
<span class="m-0 fw-bold alert-header-text">
{{ alertHeadline }}
</span>
<button
v-if="isCollapsible"
class="btn-collapse"
type="button"
@click="toggleCollapse">
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="9" viewBox="0 0 15 9"
class="btn-collapse-icon"
:class="{ 'rotated': !collapsed }">
<path fill-rule="nonzero" d="M7.5 0a.806.806 0 0 0-.593.265L.246 7.455a.957.957 0 0 0 0 1.28.796.796 0 0 0 1.185 0l6.07-6.55 6.068 6.55a.796.796 0 0 0 1.186 0 .957.957 0 0 0 0-1.28L8.093.265A.806.806 0 0 0 7.5 0"/>
</svg>
</button>
<button
v-if="isDismissible"
type="button"
class="btn-close p-2"
data-bs-dismiss="alert"
aria-label="Close">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 23.7 23.7"
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" />
</svg>
</button>
</div>
<div class="alert-body"
v-show="alertCopy"
:id="collapseId">
<template
v-for="paragraph in splitAlertCopyForParagraphTag"
:key="paragraph">
<p
v-if="!doesCopyContainRouterLink(paragraph) && !doesCopyContainTextLink(paragraph)"
class="m-0 text-body"
v-html="paragraph"></p>
<p
v-else
class="m-0 text-body">
<template v-if="doesCopyContainRouterLink(paragraph)">
<textBlock
:customText="paragraph"
class="mb-1"
marginTopSizeOverride="0" />
</template>
<template
v-for="copy in splitCopyOnCMSPlaceHolder(paragraph)"
v-else
:key="copy">
<span v-if="doesCopyContainTextLink(copy)">
<textLink
linkType="text"
:text="getRouterLinkDisplayTextFromCopy(copy)"
href="#!"
:data-bs-target="'#' + getRouterLinkRouteFromCopy(copy)"
@click-event="
$emit('textLinkClicked', getRouterLinkRouteFromCopy(copy))
" />
</span>
<span
v-else
v-html="copy"></span>
</template>
</p>
</template>
</div>
</div>
</template>
@ -74,6 +95,7 @@ import {
import applicationConfig from '@/constants/application-config';
import textBlock from '@/digital-components/text-block/text-block.vue';
import textLink from '@/ux-components/text-link/text-link.vue';
import { Collapse } from 'bootstrap';
export default {
// eslint-disable-next-line vue/multi-word-component-names
@ -84,13 +106,14 @@ export default {
},
props: {
isDismissible: Boolean,
isCollapsible: Boolean,
/*
alertClass class names:
alert-success (green)
alert-danger (red)
alert-warning (yellow)
alert-info (blue)
*/
alertClass class names:
alert-success (green)
alert-danger (red)
alert-warning (yellow)
alert-info (blue)
*/
alertClass: String,
cmsWidgetName: {
type: String,
@ -106,7 +129,14 @@ export default {
shouldScrollToOnMount: {
type: Boolean,
default: true
}
},
startCollapsed: Boolean
},
data() {
return {
collapsed: this.startCollapsed || false,
collapseElement: null,
};
},
computed: {
pageQueryString() {
@ -124,10 +154,18 @@ export default {
},
splitAlertCopyForParagraphTag() {
return splitCMSCopyOnParagraphTag(this.alertCopy);
}
},
collapseId() {
return this.isCollapsible
? `${this.cmsWidgetName}-collapse`
: null;
},
},
mounted() {
this.ensureAlertIsInViewPort();
if (this.isCollapsible) {
this.collapseElement = Collapse.getOrCreateInstance(document.getElementById(this.collapseId));
}
},
methods: {
doesCopyContainRouterLink,
@ -152,37 +190,83 @@ export default {
<= (window.innerHeight - footerHeight
|| document.documentElement.clientHeight - footerHeight)
);
}
},
toggleCollapse() {
this.collapsed = !this.collapsed;
if (this.collapsed) {
this.collapseElement.hide();
} else {
this.collapseElement.show();
}
},
}
};
</script>
<style lang="scss" scoped>
.alert {
border: 1px solid;
padding: 0rem;
margin-bottom: 2rem;
.alert-header {
display: flex;
align-items: center;
justify-content: center;
flex-direction: row;
padding: .75rem 1rem .75rem 1rem;
font-weight: 600;
color: $black;
}
.alert-icon {
min-width: 1rem;
max-height: 1rem;
}
.alert-header-text {
flex-grow: 1;
padding: 0rem .625rem;
}
.btn-collapse {
display: flex;
border: none;
background: none;
min-width: 1rem;
max-height: 1rem;
padding: 0;
}
.btn-collapse-icon {
height: 100%;
width: 100%;
transition: transform 0.3s;
transform: rotate(180deg);
&.rotated {
transform: none;
}
}
a {
text-decoration: none;
font-weight: $font-weight-normal;
}
border-color: transparent;
button {
display: none;
}
.btn-close {
background: none;
opacity: 1;
width: 0.75rem;
height: 0.75rem;
top: 2px;
right: 2px;
}
&.alert-dismissible {
button {
display: flex;
top: 2px;
right: 2px;
}
.alert-body {
margin: 0 1rem;
padding: .5rem 1rem .75rem 1rem;
border-top: 1px solid;
color: $darker-gray;
line-height: 26px;
font-weight: 400;
}
&.alert-info {
background-color: $blue-100;
.alert-heading {
color: $blue-700;
border-color: $blue-200;
.alert-body {
border-top-color: $blue-200;
}
svg {
fill: $blue-700;
@ -191,9 +275,10 @@ export default {
}
}
&.alert-danger {
background-color: $red-100;
.alert-heading {
color: $red-600;
background-color: $alert-red-bg;
border-color: $alert-red-color;
.alert-body {
border-top-color: $alert-red-color;
}
svg {
fill: $red-600;
@ -202,20 +287,20 @@ export default {
}
}
&.alert-warning {
background-color: $yellow-100;
.alert-heading {
color: $yellow-600;
background-color: $alert-yellow-bg;
border-color: $alert-yellow-color;
.alert-body {
border-top-color: $alert-yellow-color;
}
svg {
fill: $yellow-600;
width: 1rem;
height: 1rem;
fill: $alert-yellow-color;
}
}
&.alert-success {
background-color: $green-100;
.alert-heading {
color: $green-700;
border-color: $green-200;
.alert-body {
border-top-color: $green-200;
}
svg {
fill: $green-700;
@ -223,9 +308,5 @@ export default {
height: 1rem;
}
}
& p {
font-size: 0.875rem;
margin-bottom: 0.25rem !important;
}
}
</style>

View file

@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { getMountOptions } from '@/helpers/unit-test-helper.js';
import buttonMain from '@/ux-components/button-main/button-main.vue';
import buttonVariants from '@/constants/button-variants';
import { buttonVariants } from '@/constants/component-variants';
import buttonSizes from '@/constants/button-sizes';
/** @ignore */

View file

@ -9,7 +9,7 @@
</template>
<script>
import buttonVariants from '@/constants/button-variants';
import { buttonVariants } from '@/constants/component-variants';
import buttonSizes from '@/constants/button-sizes';
export default {

View file

@ -5,7 +5,10 @@
buttonWrapperClasses="list-group base-input-button list-button no-hover d-flex flex-column w-100 mb-2">
<div
:aria-label="buttonLabel"
class="button-content list-button-content d-flex flex-column justify-content-center py-3 px-4">
class="button-content list-button-content d-flex flex-column justify-content-center py-3 px-4"
:class="{
'has-sub-copy': buttonLabelSubCopy,
}">
<span
class="m-0"
:class="textPosition">
@ -85,6 +88,10 @@ $heritage-checked-border-color: #0070d1;
background: $heritage-checked-background-color;
border-color: $heritage-checked-border-color;
box-shadow: 0 0 0 1px $blue;
&:not(.has-sub-copy) {
font-weight: 500;
color: $black;
}
}
&:checked + .list-button-content span:nth-child(2) {
font-weight: 400;
@ -104,10 +111,15 @@ $heritage-checked-border-color: #0070d1;
width: 100%;
outline: none;
&:not(.has-sub-copy) {
font-weight: 400;
color: $darker-gray;
}
span {
&.small {
color: $darker-gray;
}
}
}
</style>
</style>

View file

@ -62,7 +62,7 @@ export default {
<style lang="scss" scoped>
a {
color: #0070d1;
color: $heritage-blue-primary;
text-decoration: none;
line-height: 1.56;
padding: 0;