diff --git a/app/javascript/dashboard/components/CustomAttribute.vue b/app/javascript/dashboard/components/CustomAttribute.vue index e42660ed7..4bf97320b 100644 --- a/app/javascript/dashboard/components/CustomAttribute.vue +++ b/app/javascript/dashboard/components/CustomAttribute.vue @@ -5,7 +5,7 @@ import { BUS_EVENTS } from 'shared/constants/busEvents'; import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue'; import HelperTextPopup from 'dashboard/components/ui/HelperTextPopup.vue'; import { isValidURL } from '../helper/URLHelper'; -import customAttributeMixin from '../mixins/customAttributeMixin'; +import { getRegexp } from 'shared/helpers/Validators'; import { useVuelidate } from '@vuelidate/core'; const DATE_FORMAT = 'yyyy-MM-dd'; @@ -15,7 +15,6 @@ export default { MultiselectDropdown, HelperTextPopup, }, - mixins: [customAttributeMixin], props: { label: { type: String, required: true }, description: { type: String, default: '' }, @@ -128,8 +127,7 @@ export default { required, regexValidation: value => { return !( - this.attributeRegex && - !this.getRegexp(this.attributeRegex).test(value) + this.attributeRegex && !getRegexp(this.attributeRegex).test(value) ); }, }, diff --git a/app/javascript/dashboard/mixins/customAttributeMixin.js b/app/javascript/dashboard/mixins/customAttributeMixin.js deleted file mode 100644 index a0617685d..000000000 --- a/app/javascript/dashboard/mixins/customAttributeMixin.js +++ /dev/null @@ -1,11 +0,0 @@ -export default { - methods: { - getRegexp(regexPatternValue) { - let lastSlash = regexPatternValue.lastIndexOf('/'); - return new RegExp( - regexPatternValue.slice(1, lastSlash), - regexPatternValue.slice(lastSlash + 1) - ); - }, - }, -}; diff --git a/app/javascript/dashboard/routes/dashboard/settings/attributes/EditAttribute.vue b/app/javascript/dashboard/routes/dashboard/settings/attributes/EditAttribute.vue index 0d237d866..03d78e8d1 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/attributes/EditAttribute.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/attributes/EditAttribute.vue @@ -2,11 +2,10 @@ import { useVuelidate } from '@vuelidate/core'; import { useAlert } from 'dashboard/composables'; import { required, minLength } from '@vuelidate/validators'; +import { getRegexp } from 'shared/helpers/Validators'; import { ATTRIBUTE_TYPES } from './constants'; -import customAttributeMixin from '../../../../mixins/customAttributeMixin'; export default { components: {}, - mixins: [customAttributeMixin], props: { selectedAttribute: { type: Object, @@ -116,7 +115,7 @@ export default { }, setFormValues() { const regexPattern = this.selectedAttribute.regex_pattern - ? this.getRegexp(this.selectedAttribute.regex_pattern).source + ? getRegexp(this.selectedAttribute.regex_pattern).source : null; this.displayName = this.selectedAttribute.attribute_display_name; this.description = this.selectedAttribute.attribute_description; diff --git a/app/javascript/shared/helpers/Validators.js b/app/javascript/shared/helpers/Validators.js index d8cf0b352..6502fef39 100644 --- a/app/javascript/shared/helpers/Validators.js +++ b/app/javascript/shared/helpers/Validators.js @@ -1,22 +1,58 @@ +/** + * Checks if a string is a valid E.164 phone number format. + * @param {string} value - The phone number to validate. + * @returns {boolean} True if the number is in E.164 format, false otherwise. + */ export const isPhoneE164 = value => !!value.match(/^\+[1-9]\d{1,14}$/); +/** + * Validates a phone number after removing the dial code. + * @param {string} value - The full phone number including dial code. + * @param {string} dialCode - The dial code to remove before validation. + * @returns {boolean} True if the number (without dial code) is valid, false otherwise. + */ export const isPhoneNumberValid = (value, dialCode) => { const number = value.replace(dialCode, ''); return !!number.match(/^[0-9]{1,14}$/); }; +/** + * Checks if a string is either a valid E.164 phone number or empty. + * @param {string} value - The phone number to validate. + * @returns {boolean} True if the number is in E.164 format or empty, false otherwise. + */ export const isPhoneE164OrEmpty = value => isPhoneE164(value) || value === ''; +/** + * Validates a phone number with dial code, requiring at least 5 digits. + * @param {string} value - The full phone number including dial code. + * @returns {boolean} True if the number is valid, false otherwise. + */ export const isPhoneNumberValidWithDialCode = value => { const number = value.replace(/^\+/, ''); // Remove the '+' sign return !!number.match(/^[1-9]\d{4,}$/); // Validate the phone number with minimum 5 digits }; +/** + * Checks if a string starts with a plus sign. + * @param {string} value - The string to check. + * @returns {boolean} True if the string starts with '+', false otherwise. + */ export const startsWithPlus = value => value.startsWith('+'); +/** + * Checks if a string is a valid URL (starts with 'http') or is empty. + * @param {string} [value=''] - The string to check. + * @returns {boolean} True if the string is a valid URL or empty, false otherwise. + */ export const shouldBeUrl = (value = '') => value ? value.startsWith('http') : true; +/** + * Validates a password for complexity requirements. + * @param {string} value - The password to validate. + * @returns {boolean} True if the password meets all requirements, false otherwise. + */ export const isValidPassword = value => { const containsUppercase = /[A-Z]/.test(value); const containsLowercase = /[a-z]/.test(value); @@ -32,8 +68,18 @@ export const isValidPassword = value => { ); }; +/** + * Checks if a string consists only of digits. + * @param {string} value - The string to check. + * @returns {boolean} True if the string contains only digits, false otherwise. + */ export const isNumber = value => /^\d+$/.test(value); +/** + * Validates a domain name. + * @param {string} value - The domain name to validate. + * @returns {boolean} True if the domain is valid or empty, false otherwise. + */ export const isDomain = value => { if (value !== '') { const domainRegex = /^([\p{L}0-9]+(-[\p{L}0-9]+)*\.)+[a-z]{2,}$/gmu; @@ -41,3 +87,16 @@ export const isDomain = value => { } return true; }; + +/** + * Creates a RegExp object from a string representation of a regular expression. + * @param {string} regexPatternValue - The string representation of the regex (e.g., '/pattern/flags'). + * @returns {RegExp} A RegExp object created from the input string. + */ +export const getRegexp = regexPatternValue => { + let lastSlash = regexPatternValue.lastIndexOf('/'); + return new RegExp( + regexPatternValue.slice(1, lastSlash), + regexPatternValue.slice(lastSlash + 1) + ); +}; diff --git a/app/javascript/shared/helpers/specs/ValidatorsHelper.spec.js b/app/javascript/shared/helpers/specs/ValidatorsHelper.spec.js index 6d65cb36e..fc15b772c 100644 --- a/app/javascript/shared/helpers/specs/ValidatorsHelper.spec.js +++ b/app/javascript/shared/helpers/specs/ValidatorsHelper.spec.js @@ -8,6 +8,7 @@ import { isPhoneNumberValid, isNumber, isDomain, + getRegexp, } from '../Validators'; describe('#shouldBeUrl', () => { @@ -115,3 +116,40 @@ describe('#startsWithPlus', () => { expect(startsWithPlus('123456789')).toEqual(false); }); }); + +describe('#getRegexp', () => { + it('should create a correct RegExp object', () => { + const regexPattern = '/^[a-z]+$/i'; + const regex = getRegexp(regexPattern); + + expect(regex).toBeInstanceOf(RegExp); + expect(regex.toString()).toBe(regexPattern); + + expect(regex.test('abc')).toBe(true); + expect(regex.test('ABC')).toBe(true); + expect(regex.test('123')).toBe(false); + }); + + it('should handle regex with flags', () => { + const regexPattern = '/hello/gi'; + const regex = getRegexp(regexPattern); + + expect(regex).toBeInstanceOf(RegExp); + expect(regex.toString()).toBe(regexPattern); + + expect(regex.test('hello')).toBe(true); + expect(regex.test('HELLO')).toBe(false); + expect(regex.test('Hello World')).toBe(true); + }); + + it('should handle regex with special characters', () => { + const regexPattern = '/\\d{3}-\\d{2}-\\d{4}/'; + const regex = getRegexp(regexPattern); + + expect(regex).toBeInstanceOf(RegExp); + expect(regex.toString()).toBe(regexPattern); + + expect(regex.test('123-45-6789')).toBe(true); + expect(regex.test('12-34-5678')).toBe(false); + }); +}); diff --git a/app/javascript/widget/components/PreChat/Form.vue b/app/javascript/widget/components/PreChat/Form.vue index 9b8da3798..596ba16d9 100644 --- a/app/javascript/widget/components/PreChat/Form.vue +++ b/app/javascript/widget/components/PreChat/Form.vue @@ -3,25 +3,19 @@ import CustomButton from 'shared/components/Button.vue'; import Spinner from 'shared/components/Spinner.vue'; import { mapGetters } from 'vuex'; import { getContrastingTextColor } from '@chatwoot/utils'; -import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; import { isEmptyObject } from 'widget/helpers/utils'; +import { getRegexp } from 'shared/helpers/Validators'; +import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; import routerMixin from 'widget/mixins/routerMixin'; import darkModeMixin from 'widget/mixins/darkModeMixin'; import configMixin from 'widget/mixins/configMixin'; -import customAttributeMixin from '../../../dashboard/mixins/customAttributeMixin'; export default { components: { CustomButton, Spinner, }, - mixins: [ - routerMixin, - darkModeMixin, - messageFormatterMixin, - configMixin, - customAttributeMixin, - ], + mixins: [routerMixin, darkModeMixin, messageFormatterMixin, configMixin], props: { options: { type: Object, @@ -184,7 +178,7 @@ export default { return this.formValues[name] || null; }, getValidation({ type, name, field_type, regex_pattern }) { - let regex = regex_pattern ? this.getRegexp(regex_pattern) : null; + let regex = regex_pattern ? getRegexp(regex_pattern) : null; const validations = { emailAddress: 'email', phoneNumber: ['startsWithPlus', 'isValidPhoneNumber'],