feat: Support Regex validation for custom attributes (#7856)

This allows a user to add/update a custom regex and a cue while defining custom attributes(Only applicable for type- text).
While adding/editing custom attributes, the values are validated against the attribute definition regex, and if it is incorrect, a cue message or default error message is shown and restricts invalid values from being saved.

Fixes: #6866
This commit is contained in:
Surabhi Suman
2024-01-23 19:31:57 +05:30
committed by GitHub
parent 834c219b9b
commit 4b40c61201
20 changed files with 247 additions and 22 deletions

View File

@@ -39,6 +39,8 @@ class Api::V1::Accounts::CustomAttributeDefinitionsController < Api::V1::Account
:attribute_display_type, :attribute_display_type,
:attribute_key, :attribute_key,
:attribute_model, :attribute_model,
:regex_pattern,
:regex_cue,
attribute_values: [] attribute_values: []
) )
end end

View File

@@ -126,18 +126,26 @@ import { required, url } from 'vuelidate/lib/validators';
import { BUS_EVENTS } from 'shared/constants/busEvents'; import { BUS_EVENTS } from 'shared/constants/busEvents';
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue'; import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
import { isValidURL } from '../helper/URLHelper'; import { isValidURL } from '../helper/URLHelper';
import customAttributeMixin from '../mixins/customAttributeMixin';
const DATE_FORMAT = 'yyyy-MM-dd'; const DATE_FORMAT = 'yyyy-MM-dd';
export default { export default {
components: { components: {
MultiselectDropdown, MultiselectDropdown,
}, },
mixins: [customAttributeMixin],
props: { props: {
label: { type: String, required: true }, label: { type: String, required: true },
values: { type: Array, default: () => [] }, values: { type: Array, default: () => [] },
value: { type: [String, Number, Boolean], default: '' }, value: { type: [String, Number, Boolean], default: '' },
showActions: { type: Boolean, default: false }, showActions: { type: Boolean, default: false },
attributeType: { type: String, default: 'text' }, attributeType: { type: String, default: 'text' },
attributeRegex: {
type: String,
default: null,
},
regexCue: { type: String, default: null },
regexEnabled: { type: Boolean, default: false },
attributeKey: { type: String, required: true }, attributeKey: { type: String, required: true },
contactId: { type: Number, default: null }, contactId: { type: Number, default: null },
}, },
@@ -204,6 +212,11 @@ export default {
if (this.$v.editedValue.url) { if (this.$v.editedValue.url) {
return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.INVALID_URL'); return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.INVALID_URL');
} }
if (!this.$v.editedValue.regexValidation) {
return this.regexCue
? this.regexCue
: this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.INVALID_INPUT');
}
return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.REQUIRED'); return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.REQUIRED');
}, },
}, },
@@ -221,7 +234,15 @@ export default {
}; };
} }
return { return {
editedValue: { required }, editedValue: {
required,
regexValidation: value => {
return !(
this.attributeRegex &&
!this.getRegexp(this.attributeRegex).test(value)
);
},
},
}; };
}, },
mounted() { mounted() {

View File

@@ -47,6 +47,8 @@ export const getCustomFields = ({ standardFields, customAttributes }) => {
type: attribute.attribute_display_type, type: attribute.attribute_display_type,
values: attribute.attribute_values, values: attribute.attribute_values,
field_type: attribute.attribute_model, field_type: attribute.attribute_model,
regex_pattern: attribute.regex_pattern,
regex_cue: attribute.regex_cue,
required: false, required: false,
enabled: false, enabled: false,
}); });

View File

@@ -44,4 +44,18 @@ export default {
created_at: '2021-11-29T10:20:04.563Z', created_at: '2021-11-29T10:20:04.563Z',
}, },
], ],
customAttributesWithRegex: [
{
id: 2,
attribute_description: 'Test contact Attribute',
attribute_display_name: 'Test contact Attribute',
attribute_display_type: 'text',
attribute_key: 'test_contact_attribute',
attribute_model: 'contact_attribute',
attribute_values: Array(0),
created_at: '2023-09-20T10:20:04.563Z',
regex_pattern: '^w+$',
regex_cue: 'It should be a combination of alphabets and numbers',
},
],
}; };

View File

@@ -5,7 +5,8 @@ import {
} from '../preChat'; } from '../preChat';
import inboxFixture from './inboxFixture'; import inboxFixture from './inboxFixture';
const { customFields, customAttributes } = inboxFixture; const { customFields, customAttributes, customAttributesWithRegex } =
inboxFixture;
describe('#Pre chat Helpers', () => { describe('#Pre chat Helpers', () => {
describe('getPreChatFields', () => { describe('getPreChatFields', () => {
it('should return correct pre-chat fields form options passed', () => { it('should return correct pre-chat fields form options passed', () => {
@@ -27,7 +28,6 @@ describe('#Pre chat Helpers', () => {
placeholder: 'Please enter your email address', placeholder: 'Please enter your email address',
type: 'email', type: 'email',
field_type: 'standard', field_type: 'standard',
required: false, required: false,
enabled: false, enabled: false,
}, },
@@ -71,6 +71,26 @@ describe('#Pre chat Helpers', () => {
values: [], values: [],
}, },
]); ]);
expect(
getCustomFields({
standardFields: { pre_chat_fields: customFields.pre_chat_fields },
customAttributes: customAttributesWithRegex,
})
).toEqual([
{
enabled: false,
label: 'Test contact Attribute',
placeholder: 'Test contact Attribute',
name: 'test_contact_attribute',
required: false,
field_type: 'contact_attribute',
type: 'text',
values: [],
regex_pattern: '^w+$',
regex_cue: 'It should be a combination of alphabets and numbers',
},
]);
}); });
}); });
}); });

View File

@@ -39,6 +39,17 @@
"PLACEHOLDER": "Enter custom attribute key", "PLACEHOLDER": "Enter custom attribute key",
"ERROR": "Key is required", "ERROR": "Key is required",
"IN_VALID": "Invalid key" "IN_VALID": "Invalid key"
},
"REGEX_PATTERN": {
"LABEL": "Regex Pattern",
"PLACEHOLDER": "Please enter custom attribute regex pattern. (Optional)"
},
"REGEX_CUE": {
"LABEL": "Regex Cue",
"PLACEHOLDER": "Please enter regex pattern hint. (Optional)"
},
"ENABLE_REGEX": {
"LABEL": "Enable regex validation"
} }
}, },
"API": { "API": {
@@ -88,6 +99,17 @@
"EMPTY_RESULT": { "EMPTY_RESULT": {
"404": "There are no custom attributes created", "404": "There are no custom attributes created",
"NOT_FOUND": "There are no custom attributes configured" "NOT_FOUND": "There are no custom attributes configured"
},
"REGEX_PATTERN": {
"LABEL": "Regex Pattern",
"PLACEHOLDER": "Please enter custom attribute regex pattern. (Optional)"
},
"REGEX_CUE": {
"LABEL": "Regex Cue",
"PLACEHOLDER": "Please enter regex pattern hint. (Optional)"
},
"ENABLE_REGEX": {
"LABEL": "Enable regex validation"
} }
} }
} }

View File

@@ -339,7 +339,8 @@
}, },
"VALIDATIONS": { "VALIDATIONS": {
"REQUIRED": "Valid value is required", "REQUIRED": "Valid value is required",
"INVALID_URL": "Invalid URL" "INVALID_URL": "Invalid URL",
"INVALID_INPUT": "Invalid Input"
} }
}, },
"MERGE_CONTACTS": { "MERGE_CONTACTS": {

View File

@@ -0,0 +1,11 @@
export default {
methods: {
getRegexp(regexPatternValue) {
let lastSlash = regexPatternValue.lastIndexOf('/');
return new RegExp(
regexPatternValue.slice(1, lastSlash),
regexPatternValue.slice(lastSlash + 1)
);
},
},
};

View File

@@ -11,6 +11,8 @@
emoji="" emoji=""
:value="attribute.value" :value="attribute.value"
:show-actions="true" :show-actions="true"
:attribute-regex="attribute.regex_pattern"
:regex-cue="attribute.regex_cue"
:class="attributeClass" :class="attributeClass"
@update="onUpdate" @update="onUpdate"
@delete="onDelete" @delete="onDelete"

View File

@@ -86,6 +86,30 @@
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.ERROR') }} {{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.ERROR') }}
</label> </label>
</div> </div>
<div v-if="isAttributeTypeText">
<input
v-model="regexEnabled"
type="checkbox"
@input="toggleRegexEnabled"
/>
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.ENABLE_REGEX.LABEL') }}
</div>
<woot-input
v-if="isAttributeTypeText && isRegexEnabled"
v-model="regexPattern"
:label="$t('ATTRIBUTES_MGMT.ADD.FORM.REGEX_PATTERN.LABEL')"
type="text"
:placeholder="
$t('ATTRIBUTES_MGMT.ADD.FORM.REGEX_PATTERN.PLACEHOLDER')
"
/>
<woot-input
v-if="isAttributeTypeText && isRegexEnabled"
v-model="regexCue"
:label="$t('ATTRIBUTES_MGMT.ADD.FORM.REGEX_CUE.LABEL')"
type="text"
:placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.REGEX_CUE.PLACEHOLDER')"
/>
<div class="flex flex-row justify-end gap-2 py-2 px-0 w-full"> <div class="flex flex-row justify-end gap-2 py-2 px-0 w-full">
<woot-submit-button <woot-submit-button
:disabled="isButtonDisabled" :disabled="isButtonDisabled"
@@ -124,6 +148,9 @@ export default {
attributeModel: 0, attributeModel: 0,
attributeType: 0, attributeType: 0,
attributeKey: '', attributeKey: '',
regexPattern: null,
regexCue: null,
regexEnabled: false,
models: ATTRIBUTE_MODELS, models: ATTRIBUTE_MODELS,
types: ATTRIBUTE_TYPES, types: ATTRIBUTE_TYPES,
values: [], values: [],
@@ -163,6 +190,12 @@ export default {
isAttributeTypeList() { isAttributeTypeList() {
return this.attributeType === 6; return this.attributeType === 6;
}, },
isAttributeTypeText() {
return this.attributeType === 0;
},
isRegexEnabled() {
return this.regexEnabled;
},
}, },
validations: { validations: {
@@ -201,11 +234,18 @@ export default {
onDisplayNameChange() { onDisplayNameChange() {
this.attributeKey = convertToAttributeSlug(this.displayName); this.attributeKey = convertToAttributeSlug(this.displayName);
}, },
toggleRegexEnabled() {
this.regexEnabled = !this.regexEnabled;
},
async addAttributes() { async addAttributes() {
this.$v.$touch(); this.$v.$touch();
if (this.$v.$invalid) { if (this.$v.$invalid) {
return; return;
} }
if (!this.regexEnabled) {
this.regexPattern = null;
this.regexCue = null;
}
try { try {
await this.$store.dispatch('attributes/create', { await this.$store.dispatch('attributes/create', {
attribute_display_name: this.displayName, attribute_display_name: this.displayName,
@@ -214,6 +254,10 @@ export default {
attribute_display_type: this.attributeType, attribute_display_type: this.attributeType,
attribute_key: this.attributeKey, attribute_key: this.attributeKey,
attribute_values: this.attributeListValues, attribute_values: this.attributeListValues,
regex_pattern: this.regexPattern
? new RegExp(this.regexPattern).toString()
: null,
regex_cue: this.regexCue,
}); });
this.alertMessage = this.$t('ATTRIBUTES_MGMT.ADD.API.SUCCESS_MESSAGE'); this.alertMessage = this.$t('ATTRIBUTES_MGMT.ADD.API.SUCCESS_MESSAGE');
this.onClose(); this.onClose();

View File

@@ -70,6 +70,30 @@
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.ERROR') }} {{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.ERROR') }}
</label> </label>
</div> </div>
<div v-if="isAttributeTypeText">
<input
v-model="regexEnabled"
type="checkbox"
@input="toggleRegexEnabled"
/>
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.ENABLE_REGEX.LABEL') }}
</div>
<woot-input
v-if="isAttributeTypeText && isRegexEnabled"
v-model="regexPattern"
:label="$t('ATTRIBUTES_MGMT.ADD.FORM.REGEX_PATTERN.LABEL')"
type="text"
:placeholder="
$t('ATTRIBUTES_MGMT.ADD.FORM.REGEX_PATTERN.PLACEHOLDER')
"
/>
<woot-input
v-if="isAttributeTypeText && isRegexEnabled"
v-model="regexCue"
:label="$t('ATTRIBUTES_MGMT.ADD.FORM.REGEX_CUE.LABEL')"
type="text"
:placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.REGEX_CUE.PLACEHOLDER')"
/>
</div> </div>
<div class="flex flex-row justify-end gap-2 py-2 px-0 w-full"> <div class="flex flex-row justify-end gap-2 py-2 px-0 w-full">
<woot-button :is-loading="isUpdating" :disabled="isButtonDisabled"> <woot-button :is-loading="isUpdating" :disabled="isButtonDisabled">
@@ -88,9 +112,10 @@ import { mapGetters } from 'vuex';
import { required, minLength } from 'vuelidate/lib/validators'; import { required, minLength } from 'vuelidate/lib/validators';
import { ATTRIBUTE_TYPES } from './constants'; import { ATTRIBUTE_TYPES } from './constants';
import alertMixin from 'shared/mixins/alertMixin'; import alertMixin from 'shared/mixins/alertMixin';
import customAttributeMixin from '../../../../mixins/customAttributeMixin';
export default { export default {
components: {}, components: {},
mixins: [alertMixin], mixins: [alertMixin, customAttributeMixin],
props: { props: {
selectedAttribute: { selectedAttribute: {
type: Object, type: Object,
@@ -106,6 +131,9 @@ export default {
displayName: '', displayName: '',
description: '', description: '',
attributeType: 0, attributeType: 0,
regexPattern: null,
regexCue: null,
regexEnabled: false,
types: ATTRIBUTE_TYPES, types: ATTRIBUTE_TYPES,
show: true, show: true,
attributeKey: '', attributeKey: '',
@@ -152,6 +180,7 @@ export default {
this.isAttributeTypeList && this.isTouched && this.values.length === 0 this.isAttributeTypeList && this.isTouched && this.values.length === 0
); );
}, },
pageTitle() { pageTitle() {
return `${this.$t('ATTRIBUTES_MGMT.EDIT.TITLE')} - ${ return `${this.$t('ATTRIBUTES_MGMT.EDIT.TITLE')} - ${
this.selectedAttribute.attribute_display_name this.selectedAttribute.attribute_display_name
@@ -173,6 +202,12 @@ export default {
isAttributeTypeList() { isAttributeTypeList() {
return this.attributeType === 6; return this.attributeType === 6;
}, },
isAttributeTypeText() {
return this.attributeType === 0;
},
isRegexEnabled() {
return this.regexEnabled;
},
}, },
mounted() { mounted() {
this.setFormValues(); this.setFormValues();
@@ -189,10 +224,16 @@ export default {
this.$refs.tagInput.$el.focus(); this.$refs.tagInput.$el.focus();
}, },
setFormValues() { setFormValues() {
const regexPattern = this.selectedAttribute.regex_pattern
? this.getRegexp(this.selectedAttribute.regex_pattern).source
: null;
this.displayName = this.selectedAttribute.attribute_display_name; this.displayName = this.selectedAttribute.attribute_display_name;
this.description = this.selectedAttribute.attribute_description; this.description = this.selectedAttribute.attribute_description;
this.attributeType = this.selectedAttributeType; this.attributeType = this.selectedAttributeType;
this.attributeKey = this.selectedAttribute.attribute_key; this.attributeKey = this.selectedAttribute.attribute_key;
this.regexPattern = regexPattern;
this.regexCue = this.selectedAttribute.regex_cue;
this.regexEnabled = regexPattern != null;
this.values = this.setAttributeListValue; this.values = this.setAttributeListValue;
}, },
async editAttributes() { async editAttributes() {
@@ -200,14 +241,21 @@ export default {
if (this.$v.$invalid) { if (this.$v.$invalid) {
return; return;
} }
if (!this.regexEnabled) {
this.regexPattern = null;
this.regexCue = null;
}
try { try {
await this.$store.dispatch('attributes/update', { await this.$store.dispatch('attributes/update', {
id: this.selectedAttribute.id, id: this.selectedAttribute.id,
attribute_description: this.description, attribute_description: this.description,
attribute_display_name: this.displayName, attribute_display_name: this.displayName,
attribute_values: this.updatedAttributeListValues, attribute_values: this.updatedAttributeListValues,
regex_pattern: this.regexPattern
? new RegExp(this.regexPattern).toString()
: null,
regex_cue: this.regexCue,
}); });
this.alertMessage = this.$t('ATTRIBUTES_MGMT.EDIT.API.SUCCESS_MESSAGE'); this.alertMessage = this.$t('ATTRIBUTES_MGMT.EDIT.API.SUCCESS_MESSAGE');
this.onClose(); this.onClose();
} catch (error) { } catch (error) {
@@ -218,6 +266,9 @@ export default {
this.showAlert(this.alertMessage); this.showAlert(this.alertMessage);
} }
}, },
toggleRegexEnabled() {
this.regexEnabled = !this.regexEnabled;
},
}, },
}; };
</script> </script>

View File

@@ -28,6 +28,9 @@
isValidPhoneNumber: $t('PRE_CHAT_FORM.FIELDS.PHONE_NUMBER.VALID_ERROR'), isValidPhoneNumber: $t('PRE_CHAT_FORM.FIELDS.PHONE_NUMBER.VALID_ERROR'),
email: $t('PRE_CHAT_FORM.FIELDS.EMAIL_ADDRESS.VALID_ERROR'), email: $t('PRE_CHAT_FORM.FIELDS.EMAIL_ADDRESS.VALID_ERROR'),
required: $t('PRE_CHAT_FORM.REQUIRED'), required: $t('PRE_CHAT_FORM.REQUIRED'),
matches: item.regex_cue
? item.regex_cue
: $t('PRE_CHAT_FORM.REGEX_ERROR'),
}" }"
:has-error-in-phone-input="hasErrorInPhoneInput" :has-error-in-phone-input="hasErrorInPhoneInput"
/> />
@@ -68,13 +71,20 @@ import { isEmptyObject } from 'widget/helpers/utils';
import routerMixin from 'widget/mixins/routerMixin'; import routerMixin from 'widget/mixins/routerMixin';
import darkModeMixin from 'widget/mixins/darkModeMixin'; import darkModeMixin from 'widget/mixins/darkModeMixin';
import configMixin from 'widget/mixins/configMixin'; import configMixin from 'widget/mixins/configMixin';
import customAttributeMixin from '../../../dashboard/mixins/customAttributeMixin';
export default { export default {
components: { components: {
CustomButton, CustomButton,
Spinner, Spinner,
}, },
mixins: [routerMixin, darkModeMixin, messageFormatterMixin, configMixin], mixins: [
routerMixin,
darkModeMixin,
messageFormatterMixin,
configMixin,
customAttributeMixin,
],
props: { props: {
options: { options: {
type: Object, type: Object,
@@ -235,30 +245,37 @@ export default {
} }
return this.formValues[name] || null; return this.formValues[name] || null;
}, },
getValidation({ type, name }) { getValidation({ type, name, field_type, regex_pattern }) {
let regex = regex_pattern ? this.getRegexp(regex_pattern) : null;
const validations = { const validations = {
emailAddress: 'email', emailAddress: 'email',
phoneNumber: 'startsWithPlus|isValidPhoneNumber', phoneNumber: ['startsWithPlus', 'isValidPhoneNumber'],
url: 'url', url: 'url',
date: 'date', date: 'date',
text: null, text: null,
select: null, select: null,
number: null, number: null,
checkbox: false, checkbox: false,
contact_attribute: regex ? [['matches', regex]] : null,
conversation_attribute: regex ? [['matches', regex]] : null,
}; };
const validationKeys = Object.keys(validations); const validationKeys = Object.keys(validations);
const isRequired = this.isContactFieldRequired(name); const isRequired = this.isContactFieldRequired(name);
const validation = isRequired ? 'bail|required' : 'bail|optional'; const validation = isRequired
? ['bail', 'required']
: ['bail', 'optional'];
if (validationKeys.includes(name) || validationKeys.includes(type)) { if (
const validationType = validations[type] || validations[name]; validationKeys.includes(name) ||
const validationString = validationType validationKeys.includes(type) ||
? `${validation}|${validationType}` validationKeys.includes(field_type)
: validation; ) {
return validationString; const validationType =
validations[type] || validations[name] || validations[field_type];
return validationType ? validation.concat(validationType) : validation;
} }
return ''; return [];
}, },
findFieldType(type) { findFieldType(type) {
if (type === 'link') { if (type === 'link') {

View File

@@ -80,7 +80,8 @@
}, },
"CAMPAIGN_HEADER": "Please provide your name and email before starting the conversation", "CAMPAIGN_HEADER": "Please provide your name and email before starting the conversation",
"IS_REQUIRED": "is required", "IS_REQUIRED": "is required",
"REQUIRED": "Required" "REQUIRED": "Required",
"REGEX_ERROR": "Please provide a valid input"
}, },
"FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_FILE_UPLOAD_SIZE} attachment limit", "FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_FILE_UPLOAD_SIZE} attachment limit",
"CHAT_FORM": { "CHAT_FORM": {

View File

@@ -12,7 +12,9 @@ class Inboxes::UpdateWidgetPreChatCustomFieldsJob < ApplicationJob
pre_chat_field.deep_merge({ pre_chat_field.deep_merge({
'label' => custom_attribute['attribute_display_name'], 'label' => custom_attribute['attribute_display_name'],
'placeholder' => custom_attribute['attribute_display_name'], 'placeholder' => custom_attribute['attribute_display_name'],
'values' => custom_attribute['attribute_values'] 'values' => custom_attribute['attribute_values'],
'regex_pattern' => custom_attribute['regex_pattern'],
'regex_cue' => custom_attribute['regex_cue']
}) })
end end
web_widget.save! web_widget.save!

View File

@@ -35,7 +35,7 @@ class Channel::WebWidget < ApplicationRecord
{ pre_chat_form_options: [:pre_chat_message, :require_email, { pre_chat_form_options: [:pre_chat_message, :require_email,
{ pre_chat_fields: { pre_chat_fields:
[:field_type, :label, :placeholder, :name, :enabled, :type, :enabled, :required, [:field_type, :label, :placeholder, :name, :enabled, :type, :enabled, :required,
:locale, { values: [] }] }] }, :locale, { values: [] }, :regex_pattern, :regex_cue] }] },
{ selected_feature_flags: [] }].freeze { selected_feature_flags: [] }].freeze
before_validation :validate_pre_chat_options before_validation :validate_pre_chat_options

View File

@@ -10,6 +10,8 @@
# attribute_model :integer default("conversation_attribute") # attribute_model :integer default("conversation_attribute")
# attribute_values :jsonb # attribute_values :jsonb
# default_value :integer # default_value :integer
# regex_cue :string
# regex_pattern :string
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# account_id :bigint # account_id :bigint

View File

@@ -3,6 +3,8 @@ json.attribute_display_name resource.attribute_display_name
json.attribute_display_type resource.attribute_display_type json.attribute_display_type resource.attribute_display_type
json.attribute_description resource.attribute_description json.attribute_description resource.attribute_description
json.attribute_key resource.attribute_key json.attribute_key resource.attribute_key
json.regex_pattern resource.regex_pattern
json.regex_cue resource.regex_cue
json.attribute_values resource.attribute_values json.attribute_values resource.attribute_values
json.attribute_model resource.attribute_model json.attribute_model resource.attribute_model
json.default_value resource.default_value json.default_value resource.default_value

View File

@@ -0,0 +1,6 @@
class AddRegexToCustomAttributeDefinition < ActiveRecord::Migration[7.0]
def change
add_column :custom_attribute_definitions, :regex_pattern, :string
add_column :custom_attribute_definitions, :regex_cue, :string
end
end

View File

@@ -501,6 +501,8 @@ ActiveRecord::Schema[7.0].define(version: 2023_12_19_073832) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.text "attribute_description" t.text "attribute_description"
t.jsonb "attribute_values", default: [] t.jsonb "attribute_values", default: []
t.string "regex_pattern"
t.string "regex_cue"
t.index ["account_id"], name: "index_custom_attribute_definitions_on_account_id" t.index ["account_id"], name: "index_custom_attribute_definitions_on_account_id"
t.index ["attribute_key", "attribute_model", "account_id"], name: "attribute_key_model_index", unique: true t.index ["attribute_key", "attribute_model", "account_id"], name: "attribute_key_model_index", unique: true
end end

View File

@@ -11,7 +11,9 @@ RSpec.describe Inboxes::UpdateWidgetPreChatCustomFieldsJob do
pre_chat_message = 'Share your queries here.' pre_chat_message = 'Share your queries here.'
custom_attribute = { custom_attribute = {
'attribute_key' => 'developer_id', 'attribute_key' => 'developer_id',
'attribute_display_name' => 'Developer Number' 'attribute_display_name' => 'Developer Number',
'regex_pattern' => '^[0-9]*',
'regex_cue' => 'It should be only digits'
} }
let!(:account) { create(:account) } let!(:account) { create(:account) }
let!(:web_widget) do let!(:web_widget) do
@@ -23,7 +25,8 @@ RSpec.describe Inboxes::UpdateWidgetPreChatCustomFieldsJob do
described_class.perform_now(account, custom_attribute) described_class.perform_now(account, custom_attribute)
expect(web_widget.reload.pre_chat_form_options['pre_chat_fields']).to eq [ expect(web_widget.reload.pre_chat_form_options['pre_chat_fields']).to eq [
{ 'label' => 'Developer Number', 'name' => 'developer_id', 'placeholder' => 'Developer Number', { 'label' => 'Developer Number', 'name' => 'developer_id', 'placeholder' => 'Developer Number',
'values' => nil }, { 'label' => 'Full Name', 'name' => 'full_name' } 'values' => nil, 'regex_pattern' => '^[0-9]*', 'regex_cue' => 'It should be only digits' },
{ 'label' => 'Full Name', 'name' => 'full_name' }
] ]
end end
end end