fix: regex validation not applied for custom text attributes in UI (#14110)

# Pull Request Template

## Description

This PR fixes multiple issues related to regex patterns and validation
for custom attributes.
1. Fixed regex patterns being double-escaped when saving from Add and
Edit flows
2. Fixed regex validation not being enforced in the widget pre-chat form
3. Minor UI improvements in the Add/Edit custom attribute dialog


Fixes
[CW-6625](https://linear.app/chatwoot/issue/CW-6625/bug-report-custom-attribute-regex-validation-not-working-in-ui),
https://github.com/chatwoot/chatwoot/issues/13771

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

**Loom video**

**Before**
https://www.loom.com/share/14f1983a8bc84f9fabc3663afd83cd50

**After**
https://www.loom.com/share/867c0484741140c1944fcbd43914c9c0

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
This commit is contained in:
Sivin Varghese
2026-05-05 12:55:53 +05:30
committed by GitHub
parent 21c0f4dc52
commit 6386eec5e7
7 changed files with 109 additions and 73 deletions
@@ -140,24 +140,15 @@ export default {
},
},
methods: {
labelClass(input) {
const { state } = input.context;
const hasErrors = state.invalid;
return !hasErrors ? 'text-n-slate-12' : 'text-n-ruby-10';
},
inputClass(input) {
const { state, family: classification, type } = input.context;
const hasErrors = state.invalid;
if (classification === 'box' && type === 'checkbox') {
return '';
}
if (type === 'phoneInput') {
this.hasErrorInPhoneInput = hasErrors;
this.hasErrorInPhoneInput = state.invalid;
}
if (!hasErrors) {
return `mt-1 rounded w-full py-2 px-3`;
}
return `mt-1 rounded w-full py-2 px-3 error`;
return 'mt-1 rounded w-full py-2 px-3';
},
isContactFieldRequired(field) {
return this.preChatFields.find(option => option.name === field).required;
@@ -176,7 +167,12 @@ export default {
return this.formValues[name] || null;
},
getValidation({ type, name, field_type, regex_pattern }) {
let regex = regex_pattern ? getRegexp(regex_pattern) : null;
const regex = regex_pattern ? getRegexp(regex_pattern) : null;
// FormKit caches the RegExp and calls .test() across keystrokes, so
// drop stateful g/y flags to stop lastIndex mutation flipping validity.
const matchRegex = regex
? new RegExp(regex.source, regex.flags.replace(/[gy]/g, ''))
: null;
const validations = {
emailAddress: 'email',
phoneNumber: ['startsWithPlus', 'isValidPhoneNumber'],
@@ -186,27 +182,32 @@ export default {
select: null,
number: null,
checkbox: false,
contact_attribute: regex ? [['matches', regex]] : null,
conversation_attribute: regex ? [['matches', regex]] : null,
contact_attribute: matchRegex ? [['matches', matchRegex]] : null,
conversation_attribute: matchRegex ? [['matches', matchRegex]] : null,
};
const validationKeys = Object.keys(validations);
const isRequired = this.isContactFieldRequired(name);
const validation = isRequired ? ['required'] : ['optional'];
const baseRules = isRequired ? [['required']] : [['optional']];
if (
validationKeys.includes(name) ||
validationKeys.includes(type) ||
validationKeys.includes(field_type)
!validationKeys.includes(name) &&
!validationKeys.includes(type) &&
!validationKeys.includes(field_type)
) {
const validationType =
validations[type] || validations[name] || validations[field_type];
const allValidations = validationType
? validation.concat(validationType)
: validation;
return allValidations.join('|');
return '';
}
return '';
const validationType =
validations[type] || validations[name] || validations[field_type];
if (!validationType) return baseRules;
// Normalise into array-of-arrays so RegExp objects in `['matches', regex]`
// survive without being stringified by FormKit.
const extraRules = Array.isArray(validationType)
? validationType.map(rule => (Array.isArray(rule) ? rule : [rule]))
: [[validationType]];
return baseRules.concat(extraRules);
},
findFieldType(type) {
if (type === 'link') {
@@ -283,7 +284,7 @@ export default {
}
: undefined
"
:label-class="context => `text-sm font-medium ${labelClass(context)}`"
label-class="text-sm font-medium text-n-slate-12"
:input-class="context => inputClass(context)"
:validation-messages="{
startsWithPlus: $t(
@@ -302,7 +303,7 @@ export default {
v-if="!hasActiveCampaign"
name="message"
type="textarea"
:label-class="context => `text-sm font-medium ${labelClass(context)}`"
label-class="text-sm font-medium text-n-slate-12"
:input-class="context => inputClass(context)"
:label="$t('PRE_CHAT_FORM.FIELDS.MESSAGE.LABEL')"
:placeholder="$t('PRE_CHAT_FORM.FIELDS.MESSAGE.PLACEHOLDER')"
@@ -330,16 +331,21 @@ export default {
@apply mt-2;
.formkit-inner {
input.error,
textarea.error,
select.error {
@apply outline-n-ruby-8 dark:outline-n-ruby-8 hover:outline-n-ruby-9 dark:hover:outline-n-ruby-9 focus:outline-n-ruby-9 dark:focus:outline-n-ruby-9;
}
input[type='checkbox'] {
@apply size-4 outline-none;
}
}
&[data-invalid] {
.formkit-label {
@apply text-n-ruby-10;
}
.formkit-inner input,
.formkit-inner textarea,
.formkit-inner select {
@apply outline-n-ruby-8 dark:outline-n-ruby-8 hover:outline-n-ruby-9 dark:hover:outline-n-ruby-9 focus:outline-n-ruby-9 dark:focus:outline-n-ruby-9;
}
}
}
[data-invalid] .formkit-message {