diff --git a/app/controllers/api/v1/accounts/custom_attribute_definitions_controller.rb b/app/controllers/api/v1/accounts/custom_attribute_definitions_controller.rb index 3840644ce..69df99e14 100644 --- a/app/controllers/api/v1/accounts/custom_attribute_definitions_controller.rb +++ b/app/controllers/api/v1/accounts/custom_attribute_definitions_controller.rb @@ -39,6 +39,8 @@ class Api::V1::Accounts::CustomAttributeDefinitionsController < Api::V1::Account :attribute_display_type, :attribute_key, :attribute_model, + :regex_pattern, + :regex_cue, attribute_values: [] ) end diff --git a/app/javascript/dashboard/components/CustomAttribute.vue b/app/javascript/dashboard/components/CustomAttribute.vue index 9b145c1f2..c4846bcbc 100644 --- a/app/javascript/dashboard/components/CustomAttribute.vue +++ b/app/javascript/dashboard/components/CustomAttribute.vue @@ -126,18 +126,26 @@ import { required, url } from 'vuelidate/lib/validators'; import { BUS_EVENTS } from 'shared/constants/busEvents'; import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue'; import { isValidURL } from '../helper/URLHelper'; +import customAttributeMixin from '../mixins/customAttributeMixin'; const DATE_FORMAT = 'yyyy-MM-dd'; export default { components: { MultiselectDropdown, }, + mixins: [customAttributeMixin], props: { label: { type: String, required: true }, values: { type: Array, default: () => [] }, value: { type: [String, Number, Boolean], default: '' }, showActions: { type: Boolean, default: false }, 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 }, contactId: { type: Number, default: null }, }, @@ -204,6 +212,11 @@ export default { if (this.$v.editedValue.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'); }, }, @@ -221,7 +234,15 @@ export default { }; } return { - editedValue: { required }, + editedValue: { + required, + regexValidation: value => { + return !( + this.attributeRegex && + !this.getRegexp(this.attributeRegex).test(value) + ); + }, + }, }; }, mounted() { diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryChildNavItem.vue b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryChildNavItem.vue index 5517d12ae..d85fedfa5 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryChildNavItem.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryChildNavItem.vue @@ -56,7 +56,7 @@ class="bg-slate-50 dark:bg-slate-700 rounded text-xxs font-medium mx-1 py-0 px-1" :class=" isCountZero - ? `text-slate-300 dark:text-slate-700` + ? `text-slate-300 dark:text-slate-500` : `text-slate-700 dark:text-slate-50` " > diff --git a/app/javascript/dashboard/helper/preChat.js b/app/javascript/dashboard/helper/preChat.js index 14d062707..ec4158205 100644 --- a/app/javascript/dashboard/helper/preChat.js +++ b/app/javascript/dashboard/helper/preChat.js @@ -47,6 +47,8 @@ export const getCustomFields = ({ standardFields, customAttributes }) => { type: attribute.attribute_display_type, values: attribute.attribute_values, field_type: attribute.attribute_model, + regex_pattern: attribute.regex_pattern, + regex_cue: attribute.regex_cue, required: false, enabled: false, }); diff --git a/app/javascript/dashboard/helper/specs/inboxFixture.js b/app/javascript/dashboard/helper/specs/inboxFixture.js index 6622a6de2..4a83464ef 100644 --- a/app/javascript/dashboard/helper/specs/inboxFixture.js +++ b/app/javascript/dashboard/helper/specs/inboxFixture.js @@ -44,4 +44,18 @@ export default { 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', + }, + ], }; diff --git a/app/javascript/dashboard/helper/specs/preChat.spec.js b/app/javascript/dashboard/helper/specs/preChat.spec.js index 74f3e72f5..cec255f05 100644 --- a/app/javascript/dashboard/helper/specs/preChat.spec.js +++ b/app/javascript/dashboard/helper/specs/preChat.spec.js @@ -5,7 +5,8 @@ import { } from '../preChat'; import inboxFixture from './inboxFixture'; -const { customFields, customAttributes } = inboxFixture; +const { customFields, customAttributes, customAttributesWithRegex } = + inboxFixture; describe('#Pre chat Helpers', () => { describe('getPreChatFields', () => { it('should return correct pre-chat fields form options passed', () => { @@ -27,7 +28,6 @@ describe('#Pre chat Helpers', () => { placeholder: 'Please enter your email address', type: 'email', field_type: 'standard', - required: false, enabled: false, }, @@ -71,6 +71,26 @@ describe('#Pre chat Helpers', () => { 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', + }, + ]); }); }); }); diff --git a/app/javascript/dashboard/i18n/locale/en/attributesMgmt.json b/app/javascript/dashboard/i18n/locale/en/attributesMgmt.json index 7cf58059e..a2f7386dc 100644 --- a/app/javascript/dashboard/i18n/locale/en/attributesMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/attributesMgmt.json @@ -39,6 +39,17 @@ "PLACEHOLDER": "Enter custom attribute key", "ERROR": "Key is required", "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": { @@ -88,6 +99,17 @@ "EMPTY_RESULT": { "404": "There are no custom attributes created", "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" } } } diff --git a/app/javascript/dashboard/i18n/locale/en/contact.json b/app/javascript/dashboard/i18n/locale/en/contact.json index 7ad18b792..594e34c4f 100644 --- a/app/javascript/dashboard/i18n/locale/en/contact.json +++ b/app/javascript/dashboard/i18n/locale/en/contact.json @@ -339,7 +339,8 @@ }, "VALIDATIONS": { "REQUIRED": "Valid value is required", - "INVALID_URL": "Invalid URL" + "INVALID_URL": "Invalid URL", + "INVALID_INPUT": "Invalid Input" } }, "MERGE_CONTACTS": { diff --git a/app/javascript/dashboard/i18n/locale/en/helpCenter.json b/app/javascript/dashboard/i18n/locale/en/helpCenter.json index 1f3bda807..d94692e3d 100644 --- a/app/javascript/dashboard/i18n/locale/en/helpCenter.json +++ b/app/javascript/dashboard/i18n/locale/en/helpCenter.json @@ -324,7 +324,8 @@ "LAST_EDITED": "Last edited" }, "COLUMNS": { - "BY": "by" + "BY": "by", + "AUTHOR_NOT_AVAILABLE": "Author is not available" } }, "EDIT_ARTICLE": { diff --git a/app/javascript/dashboard/mixins/customAttributeMixin.js b/app/javascript/dashboard/mixins/customAttributeMixin.js new file mode 100644 index 000000000..a0617685d --- /dev/null +++ b/app/javascript/dashboard/mixins/customAttributeMixin.js @@ -0,0 +1,11 @@ +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/conversation/customAttributes/CustomAttributes.vue b/app/javascript/dashboard/routes/dashboard/conversation/customAttributes/CustomAttributes.vue index f038e3a42..ca721d4d8 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/customAttributes/CustomAttributes.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/customAttributes/CustomAttributes.vue @@ -11,6 +11,8 @@ emoji="" :value="attribute.value" :show-actions="true" + :attribute-regex="attribute.regex_pattern" + :regex-cue="attribute.regex_cue" :class="attributeClass" @update="onUpdate" @delete="onDelete" diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/components/ArticleItem.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/components/ArticleItem.vue index 95a5d55d0..6a4fd17f6 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/components/ArticleItem.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/components/ArticleItem.vue @@ -11,9 +11,30 @@ {{ title }} -
- {{ $t('HELP_CENTER.TABLE.COLUMNS.BY') }} - {{ articleAuthorName }} +
+ +
+ +
+ + {{ articleAuthorName }} +
@@ -57,10 +78,12 @@ import timeMixin from 'dashboard/mixins/time'; import portalMixin from '../mixins/portalMixin'; import { frontendURL } from 'dashboard/helper/URLHelper'; import EmojiOrIcon from '../../../../../shared/components/EmojiOrIcon.vue'; +import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; export default { components: { EmojiOrIcon, + Thumbnail, }, mixins: [timeMixin, portalMixin], props: { @@ -110,7 +133,7 @@ export default { }).format(this.views || 0); }, articleAuthorName() { - return this.author.name; + return this.author?.name || '-'; }, labelColor() { switch (this.status) { @@ -189,15 +212,6 @@ export default { .article-block { @apply min-w-0; } - - .author { - .by { - @apply font-normal text-slate-500 dark:text-slate-200 text-sm; - } - .name { - @apply font-normal text-slate-500 dark:text-slate-200 text-sm; - } - } } span { diff --git a/app/javascript/dashboard/routes/dashboard/settings/attributes/AddAttribute.vue b/app/javascript/dashboard/routes/dashboard/settings/attributes/AddAttribute.vue index eeceaf743..e517651d3 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/attributes/AddAttribute.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/attributes/AddAttribute.vue @@ -86,6 +86,30 @@ {{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.ERROR') }} +
+ + {{ $t('ATTRIBUTES_MGMT.ADD.FORM.ENABLE_REGEX.LABEL') }} +
+ +
+
+ + {{ $t('ATTRIBUTES_MGMT.ADD.FORM.ENABLE_REGEX.LABEL') }} +
+ +
@@ -88,9 +112,10 @@ import { mapGetters } from 'vuex'; import { required, minLength } from 'vuelidate/lib/validators'; import { ATTRIBUTE_TYPES } from './constants'; import alertMixin from 'shared/mixins/alertMixin'; +import customAttributeMixin from '../../../../mixins/customAttributeMixin'; export default { components: {}, - mixins: [alertMixin], + mixins: [alertMixin, customAttributeMixin], props: { selectedAttribute: { type: Object, @@ -106,6 +131,9 @@ export default { displayName: '', description: '', attributeType: 0, + regexPattern: null, + regexCue: null, + regexEnabled: false, types: ATTRIBUTE_TYPES, show: true, attributeKey: '', @@ -152,6 +180,7 @@ export default { this.isAttributeTypeList && this.isTouched && this.values.length === 0 ); }, + pageTitle() { return `${this.$t('ATTRIBUTES_MGMT.EDIT.TITLE')} - ${ this.selectedAttribute.attribute_display_name @@ -173,6 +202,12 @@ export default { isAttributeTypeList() { return this.attributeType === 6; }, + isAttributeTypeText() { + return this.attributeType === 0; + }, + isRegexEnabled() { + return this.regexEnabled; + }, }, mounted() { this.setFormValues(); @@ -189,10 +224,16 @@ export default { this.$refs.tagInput.$el.focus(); }, setFormValues() { + const regexPattern = this.selectedAttribute.regex_pattern + ? this.getRegexp(this.selectedAttribute.regex_pattern).source + : null; this.displayName = this.selectedAttribute.attribute_display_name; this.description = this.selectedAttribute.attribute_description; this.attributeType = this.selectedAttributeType; this.attributeKey = this.selectedAttribute.attribute_key; + this.regexPattern = regexPattern; + this.regexCue = this.selectedAttribute.regex_cue; + this.regexEnabled = regexPattern != null; this.values = this.setAttributeListValue; }, async editAttributes() { @@ -200,14 +241,21 @@ export default { if (this.$v.$invalid) { return; } + if (!this.regexEnabled) { + this.regexPattern = null; + this.regexCue = null; + } try { await this.$store.dispatch('attributes/update', { id: this.selectedAttribute.id, attribute_description: this.description, attribute_display_name: this.displayName, 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.onClose(); } catch (error) { @@ -218,6 +266,9 @@ export default { this.showAlert(this.alertMessage); } }, + toggleRegexEnabled() { + this.regexEnabled = !this.regexEnabled; + }, }, }; diff --git a/app/javascript/shared/components/FluentIcon/dashboard-icons.json b/app/javascript/shared/components/FluentIcon/dashboard-icons.json index e6471a5cd..93a876346 100644 --- a/app/javascript/shared/components/FluentIcon/dashboard-icons.json +++ b/app/javascript/shared/components/FluentIcon/dashboard-icons.json @@ -154,6 +154,7 @@ "person-add-outline": "M17.5 12a5.5 5.5 0 1 1 0 11a5.5 5.5 0 0 1 0-11zm-5.477 2a6.47 6.47 0 0 0-.709 1.5H4.253a.749.749 0 0 0-.75.75v.577c0 .535.192 1.053.54 1.46c1.253 1.469 3.22 2.214 5.957 2.214c.597 0 1.157-.035 1.68-.106c.246.495.553.954.912 1.367c-.795.16-1.66.24-2.592.24c-3.146 0-5.532-.906-7.098-2.74a3.75 3.75 0 0 1-.898-2.435v-.578A2.249 2.249 0 0 1 4.253 14h7.77zm5.477 0l-.09.008a.5.5 0 0 0-.402.402L17 14.5V17h-2.496l-.09.008a.5.5 0 0 0-.402.402l-.008.09l.008.09a.5.5 0 0 0 .402.402l.09.008H17L17 20.5l.008.09a.5.5 0 0 0 .402.402l.09.008l.09-.008a.5.5 0 0 0 .402-.402L18 20.5V18h2.504l.09-.008a.5.5 0 0 0 .402-.402l.008-.09l-.008-.09a.5.5 0 0 0-.402-.402l-.09-.008H18L18 14.5l-.008-.09a.5.5 0 0 0-.402-.402L17.5 14zM10 2.005a5 5 0 1 1 0 10a5 5 0 0 1 0-10zm0 1.5a3.5 3.5 0 1 0 0 7a3.5 3.5 0 0 0 0-7z", "person-assign-outline": "M11.313 15.5a6.471 6.471 0 0 1 .709-1.5h-7.77a2.249 2.249 0 0 0-2.249 2.25v.577c0 .892.319 1.756.899 2.435c1.566 1.834 3.952 2.74 7.098 2.74c.931 0 1.796-.08 2.592-.24a6.51 6.51 0 0 1-.913-1.366c-.524.07-1.083.105-1.68.105c-2.737 0-4.703-.745-5.957-2.213a2.25 2.25 0 0 1-.539-1.461v-.578a.75.75 0 0 1 .75-.749h7.06ZM10 2.005a5 5 0 1 1 0 10a5 5 0 0 1 0-10Zm0 1.5a3.5 3.5 0 1 0 0 7a3.5 3.5 0 0 0 0-7ZM23 17.5a5.5 5.5 0 1 1-11 0a5.5 5.5 0 0 1 11 0Zm-4.647-2.853a.5.5 0 0 0-.707.707L19.293 17H15a.5.5 0 1 0 0 1h4.293l-1.647 1.647a.5.5 0 0 0 .707.707l2.5-2.5a.497.497 0 0 0 .147-.345V17.5a.498.498 0 0 0-.15-.357l-2.497-2.496Z", "person-outline": "M17.754 14a2.249 2.249 0 0 1 2.25 2.249v.575c0 .894-.32 1.76-.902 2.438-1.57 1.834-3.957 2.739-7.102 2.739-3.146 0-5.532-.905-7.098-2.74a3.75 3.75 0 0 1-.898-2.435v-.577a2.249 2.249 0 0 1 2.249-2.25h11.501Zm0 1.5H6.253a.749.749 0 0 0-.75.749v.577c0 .536.192 1.054.54 1.461 1.253 1.468 3.219 2.214 5.957 2.214s4.706-.746 5.962-2.214a2.25 2.25 0 0 0 .541-1.463v-.575a.749.749 0 0 0-.749-.75ZM12 2.004a5 5 0 1 1 0 10 5 5 0 0 1 0-10Zm0 1.5a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7Z", + "person-filled": "M17.754 14a2.249 2.249 0 0 1 2.249 2.25v.918a2.75 2.75 0 0 1-.513 1.598c-1.545 2.164-4.07 3.235-7.49 3.235c-3.421 0-5.944-1.072-7.486-3.236a2.75 2.75 0 0 1-.51-1.596v-.92A2.249 2.249 0 0 1 6.251 14h11.502ZM12 2.005a5 5 0 1 1 0 10a5 5 0 0 1 0-10Z", "play-circle-outline": "M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12Zm8.856-3.845A1.25 1.25 0 0 0 9 9.248v5.504a1.25 1.25 0 0 0 1.856 1.093l5.757-3.189a.75.75 0 0 0 0-1.312l-5.757-3.189Z", "power-outline": "M8.204 4.82a.75.75 0 0 1 .634 1.36A7.51 7.51 0 0 0 4.5 12.991c0 4.148 3.358 7.51 7.499 7.51s7.499-3.362 7.499-7.51a7.51 7.51 0 0 0-4.323-6.804.75.75 0 1 1 .637-1.358 9.01 9.01 0 0 1 5.186 8.162c0 4.976-4.029 9.01-9 9.01C7.029 22 3 17.966 3 12.99a9.01 9.01 0 0 1 5.204-8.17ZM12 2.496a.75.75 0 0 1 .743.648l.007.102v7.5a.75.75 0 0 1-1.493.102l-.007-.102v-7.5a.75.75 0 0 1 .75-.75Z", "quote-outline": "M7.5 6a2.5 2.5 0 0 1 2.495 2.336l.005.206c-.01 3.555-1.24 6.614-3.705 9.223a.75.75 0 1 1-1.09-1.03c1.64-1.737 2.66-3.674 3.077-5.859A2.5 2.5 0 1 1 7.5 6Zm9 0a2.5 2.5 0 0 1 2.495 2.336l.005.206c-.01 3.56-1.238 6.614-3.705 9.223a.75.75 0 1 1-1.09-1.03c1.643-1.738 2.662-3.672 3.078-5.859A2.5 2.5 0 1 1 16.5 6Zm-9 1.5a1 1 0 1 0 .993 1.117l.007-.124a1 1 0 0 0-1-.993Zm9 0a1 1 0 1 0 .993 1.117l.007-.124a1 1 0 0 0-1-.993Z", diff --git a/app/javascript/widget/components/PreChat/Form.vue b/app/javascript/widget/components/PreChat/Form.vue index 50c4a22c4..394b338d5 100644 --- a/app/javascript/widget/components/PreChat/Form.vue +++ b/app/javascript/widget/components/PreChat/Form.vue @@ -28,6 +28,9 @@ isValidPhoneNumber: $t('PRE_CHAT_FORM.FIELDS.PHONE_NUMBER.VALID_ERROR'), email: $t('PRE_CHAT_FORM.FIELDS.EMAIL_ADDRESS.VALID_ERROR'), 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" /> @@ -68,13 +71,20 @@ import { isEmptyObject } from 'widget/helpers/utils'; 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], + mixins: [ + routerMixin, + darkModeMixin, + messageFormatterMixin, + configMixin, + customAttributeMixin, + ], props: { options: { type: Object, @@ -235,30 +245,37 @@ export default { } 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 = { emailAddress: 'email', - phoneNumber: 'startsWithPlus|isValidPhoneNumber', + phoneNumber: ['startsWithPlus', 'isValidPhoneNumber'], url: 'url', date: 'date', text: null, select: null, number: null, checkbox: false, + contact_attribute: regex ? [['matches', regex]] : null, + conversation_attribute: regex ? [['matches', regex]] : null, }; const validationKeys = Object.keys(validations); 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)) { - const validationType = validations[type] || validations[name]; - const validationString = validationType - ? `${validation}|${validationType}` - : validation; - return validationString; + if ( + validationKeys.includes(name) || + validationKeys.includes(type) || + validationKeys.includes(field_type) + ) { + const validationType = + validations[type] || validations[name] || validations[field_type]; + return validationType ? validation.concat(validationType) : validation; } - return ''; + return []; }, findFieldType(type) { if (type === 'link') { diff --git a/app/javascript/widget/i18n/locale/en.json b/app/javascript/widget/i18n/locale/en.json index 3a1f51ccf..83b442e51 100644 --- a/app/javascript/widget/i18n/locale/en.json +++ b/app/javascript/widget/i18n/locale/en.json @@ -80,7 +80,8 @@ }, "CAMPAIGN_HEADER": "Please provide your name and email before starting the conversation", "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", "CHAT_FORM": { diff --git a/app/jobs/inboxes/update_widget_pre_chat_custom_fields_job.rb b/app/jobs/inboxes/update_widget_pre_chat_custom_fields_job.rb index b17ecdf55..3f3326633 100644 --- a/app/jobs/inboxes/update_widget_pre_chat_custom_fields_job.rb +++ b/app/jobs/inboxes/update_widget_pre_chat_custom_fields_job.rb @@ -12,7 +12,9 @@ class Inboxes::UpdateWidgetPreChatCustomFieldsJob < ApplicationJob pre_chat_field.deep_merge({ 'label' => 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 web_widget.save! diff --git a/app/models/channel/web_widget.rb b/app/models/channel/web_widget.rb index a75e979b9..2efe74881 100644 --- a/app/models/channel/web_widget.rb +++ b/app/models/channel/web_widget.rb @@ -35,7 +35,7 @@ class Channel::WebWidget < ApplicationRecord { pre_chat_form_options: [:pre_chat_message, :require_email, { pre_chat_fields: [:field_type, :label, :placeholder, :name, :enabled, :type, :enabled, :required, - :locale, { values: [] }] }] }, + :locale, { values: [] }, :regex_pattern, :regex_cue] }] }, { selected_feature_flags: [] }].freeze before_validation :validate_pre_chat_options diff --git a/app/models/custom_attribute_definition.rb b/app/models/custom_attribute_definition.rb index 71aab06f8..7d7b36e42 100644 --- a/app/models/custom_attribute_definition.rb +++ b/app/models/custom_attribute_definition.rb @@ -10,6 +10,8 @@ # attribute_model :integer default("conversation_attribute") # attribute_values :jsonb # default_value :integer +# regex_cue :string +# regex_pattern :string # created_at :datetime not null # updated_at :datetime not null # account_id :bigint diff --git a/app/models/inbox.rb b/app/models/inbox.rb index 311d7369b..17f4fc045 100644 --- a/app/models/inbox.rb +++ b/app/models/inbox.rb @@ -66,7 +66,7 @@ class Inbox < ApplicationRecord has_many :inbox_members, dependent: :destroy_async has_many :members, through: :inbox_members, source: :user has_many :conversations, dependent: :destroy_async - has_many :messages, through: :conversations + has_many :messages, dependent: :destroy_async has_one :agent_bot_inbox, dependent: :destroy_async has_one :agent_bot, through: :agent_bot_inbox diff --git a/app/views/api/v1/models/_custom_attribute_definition.json.jbuilder b/app/views/api/v1/models/_custom_attribute_definition.json.jbuilder index 0a1036903..8a1010d45 100644 --- a/app/views/api/v1/models/_custom_attribute_definition.json.jbuilder +++ b/app/views/api/v1/models/_custom_attribute_definition.json.jbuilder @@ -3,6 +3,8 @@ json.attribute_display_name resource.attribute_display_name json.attribute_display_type resource.attribute_display_type json.attribute_description resource.attribute_description 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_model resource.attribute_model json.default_value resource.default_value diff --git a/db/migrate/20230905060223_add_regex_to_custom_attribute_definition.rb b/db/migrate/20230905060223_add_regex_to_custom_attribute_definition.rb new file mode 100644 index 000000000..dccca8cb5 --- /dev/null +++ b/db/migrate/20230905060223_add_regex_to_custom_attribute_definition.rb @@ -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 diff --git a/db/migrate/20231223033019_refactor_sla_policy_columns.rb b/db/migrate/20231223033019_refactor_sla_policy_columns.rb new file mode 100644 index 000000000..2a6913799 --- /dev/null +++ b/db/migrate/20231223033019_refactor_sla_policy_columns.rb @@ -0,0 +1,8 @@ +class RefactorSlaPolicyColumns < ActiveRecord::Migration[7.0] + def change + rename_column :sla_policies, :rt_threshold, :next_response_time_threshold + rename_column :sla_policies, :frt_threshold, :first_response_time_threshold + add_column :sla_policies, :description, :string + add_column :sla_policies, :resolution_time_threshold, :float + end +end diff --git a/db/migrate/20231223040257_create_applied_slas.rb b/db/migrate/20231223040257_create_applied_slas.rb new file mode 100644 index 000000000..1aedb7d05 --- /dev/null +++ b/db/migrate/20231223040257_create_applied_slas.rb @@ -0,0 +1,13 @@ +class CreateAppliedSlas < ActiveRecord::Migration[7.0] + def change + create_table :applied_slas do |t| + t.references :account, null: false + t.references :sla_policy, null: false + t.references :conversation, null: false + + t.string :sla_status + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 1fdf26882..0fcf7ba61 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_12_19_073832) do +ActiveRecord::Schema[7.0].define(version: 2023_12_23_040257) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" @@ -115,6 +115,18 @@ ActiveRecord::Schema[7.0].define(version: 2023_12_19_073832) do t.index ["account_id"], name: "index_agent_bots_on_account_id" end + create_table "applied_slas", force: :cascade do |t| + t.bigint "account_id", null: false + t.bigint "sla_policy_id", null: false + t.bigint "conversation_id", null: false + t.string "sla_status" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id"], name: "index_applied_slas_on_account_id" + t.index ["conversation_id"], name: "index_applied_slas_on_conversation_id" + t.index ["sla_policy_id"], name: "index_applied_slas_on_sla_policy_id" + end + create_table "articles", force: :cascade do |t| t.integer "account_id", null: false t.integer "portal_id", null: false @@ -501,6 +513,8 @@ ActiveRecord::Schema[7.0].define(version: 2023_12_19_073832) do t.datetime "updated_at", null: false t.text "attribute_description" 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 ["attribute_key", "attribute_model", "account_id"], name: "attribute_key_model_index", unique: true end @@ -822,12 +836,14 @@ ActiveRecord::Schema[7.0].define(version: 2023_12_19_073832) do create_table "sla_policies", force: :cascade do |t| t.string "name", null: false - t.float "frt_threshold" - t.float "rt_threshold" + t.float "first_response_time_threshold" + t.float "next_response_time_threshold" t.boolean "only_during_business_hours", default: false t.bigint "account_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "description" + t.float "resolution_time_threshold" t.index ["account_id"], name: "index_sla_policies_on_account_id" end diff --git a/enterprise/app/controllers/api/v1/accounts/sla_policies_controller.rb b/enterprise/app/controllers/api/v1/accounts/sla_policies_controller.rb index fa79c5362..e64256bc7 100644 --- a/enterprise/app/controllers/api/v1/accounts/sla_policies_controller.rb +++ b/enterprise/app/controllers/api/v1/accounts/sla_policies_controller.rb @@ -22,7 +22,8 @@ class Api::V1::Accounts::SlaPoliciesController < Api::V1::Accounts::EnterpriseAc end def permitted_params - params.require(:sla_policy).permit(:name, :rt_threshold, :frt_threshold, :only_during_business_hours) + params.require(:sla_policy).permit(:name, :description, :first_response_time_threshold, :next_response_time_threshold, + :resolution_time_threshold, :only_during_business_hours) end def fetch_sla diff --git a/enterprise/app/models/applied_sla.rb b/enterprise/app/models/applied_sla.rb new file mode 100644 index 000000000..8329f643b --- /dev/null +++ b/enterprise/app/models/applied_sla.rb @@ -0,0 +1,23 @@ +# == Schema Information +# +# Table name: applied_slas +# +# id :bigint not null, primary key +# sla_status :string +# created_at :datetime not null +# updated_at :datetime not null +# account_id :bigint not null +# conversation_id :bigint not null +# sla_policy_id :bigint not null +# +# Indexes +# +# index_applied_slas_on_account_id (account_id) +# index_applied_slas_on_conversation_id (conversation_id) +# index_applied_slas_on_sla_policy_id (sla_policy_id) +# +class AppliedSla < ApplicationRecord + belongs_to :account + belongs_to :sla_policy + belongs_to :conversation +end diff --git a/enterprise/app/models/sla_policy.rb b/enterprise/app/models/sla_policy.rb index 14ea1c34a..328367aeb 100644 --- a/enterprise/app/models/sla_policy.rb +++ b/enterprise/app/models/sla_policy.rb @@ -2,14 +2,16 @@ # # Table name: sla_policies # -# id :bigint not null, primary key -# frt_threshold :float -# name :string not null -# only_during_business_hours :boolean default(FALSE) -# rt_threshold :float -# created_at :datetime not null -# updated_at :datetime not null -# account_id :bigint not null +# id :bigint not null, primary key +# description :string +# first_response_time_threshold :float +# name :string not null +# next_response_time_threshold :float +# only_during_business_hours :boolean default(FALSE) +# resolution_time_threshold :float +# created_at :datetime not null +# updated_at :datetime not null +# account_id :bigint not null # # Indexes # diff --git a/enterprise/app/views/api/v1/models/_sla_policy.json.jbuilder b/enterprise/app/views/api/v1/models/_sla_policy.json.jbuilder index cd03d50ad..686b4317f 100644 --- a/enterprise/app/views/api/v1/models/_sla_policy.json.jbuilder +++ b/enterprise/app/views/api/v1/models/_sla_policy.json.jbuilder @@ -1,5 +1,7 @@ json.id sla_policy.id json.name sla_policy.name -json.frt_threshold sla_policy.frt_threshold -json.rt_threshold sla_policy.rt_threshold +json.description sla_policy.description +json.first_response_time_threshold sla_policy.first_response_time_threshold +json.next_response_time_threshold sla_policy.next_response_time_threshold +json.resolution_time_threshold sla_policy.resolution_time_threshold json.only_during_business_hours sla_policy.only_during_business_hours diff --git a/spec/enterprise/controllers/api/v1/accounts/sla_policies_controller_spec.rb b/spec/enterprise/controllers/api/v1/accounts/sla_policies_controller_spec.rb index 99a3bf0f0..b1619ef85 100644 --- a/spec/enterprise/controllers/api/v1/accounts/sla_policies_controller_spec.rb +++ b/spec/enterprise/controllers/api/v1/accounts/sla_policies_controller_spec.rb @@ -80,8 +80,10 @@ RSpec.describe 'Enterprise SLA API', type: :request do describe 'POST #create' do let(:valid_params) do { sla_policy: { name: 'SLA 2', - frt_threshold: 1000, - rt_threshold: 1000, + description: 'SLA for premium customers', + first_response_time_threshold: 1000, + next_response_time_threshold: 2000, + resolution_time_threshold: 3000, only_during_business_hours: false } } end diff --git a/spec/enterprise/models/applied_sla_spec.rb b/spec/enterprise/models/applied_sla_spec.rb new file mode 100644 index 000000000..af73395a6 --- /dev/null +++ b/spec/enterprise/models/applied_sla_spec.rb @@ -0,0 +1,16 @@ +require 'rails_helper' + +RSpec.describe AppliedSla, type: :model do + describe 'associations' do + it { is_expected.to belong_to(:sla_policy) } + it { is_expected.to belong_to(:account) } + it { is_expected.to belong_to(:conversation) } + end + + describe 'validates_factory' do + it 'creates valid applied sla policy object' do + applied_sla = create(:applied_sla) + expect(applied_sla.sla_status).to eq 'active' + end + end +end diff --git a/spec/enterprise/models/sla_policy_spec.rb b/spec/enterprise/models/sla_policy_spec.rb index 6fa8044cd..dc3c5cb87 100644 --- a/spec/enterprise/models/sla_policy_spec.rb +++ b/spec/enterprise/models/sla_policy_spec.rb @@ -18,6 +18,11 @@ RSpec.describe SlaPolicy, type: :model do it 'creates valid sla policy object' do sla_policy = create(:sla_policy) expect(sla_policy.name).to eq 'sla_1' + expect(sla_policy.first_response_time_threshold).to eq 2000 + expect(sla_policy.description).to eq 'SLA policy for enterprise customers' + expect(sla_policy.next_response_time_threshold).to eq 1000 + expect(sla_policy.resolution_time_threshold).to eq 3000 + expect(sla_policy.only_during_business_hours).to be false end end end diff --git a/spec/factories/applied_slas.rb b/spec/factories/applied_slas.rb new file mode 100644 index 000000000..8ab48c558 --- /dev/null +++ b/spec/factories/applied_slas.rb @@ -0,0 +1,8 @@ +FactoryBot.define do + factory :applied_sla do + account + sla_policy + conversation + sla_status { 'active' } + end +end diff --git a/spec/factories/sla_policies.rb b/spec/factories/sla_policies.rb index 3f1d33b43..fbbd892ce 100644 --- a/spec/factories/sla_policies.rb +++ b/spec/factories/sla_policies.rb @@ -2,8 +2,10 @@ FactoryBot.define do factory :sla_policy do account name { 'sla_1' } - rt_threshold { 1000 } - frt_threshold { 2000 } + first_response_time_threshold { 2000 } + description { 'SLA policy for enterprise customers' } + next_response_time_threshold { 1000 } + resolution_time_threshold { 3000 } only_during_business_hours { false } end end diff --git a/spec/jobs/inboxes/update_widget_pre_chat_custom_fields_job_spec.rb b/spec/jobs/inboxes/update_widget_pre_chat_custom_fields_job_spec.rb index 284a0de70..a33b2c7bd 100644 --- a/spec/jobs/inboxes/update_widget_pre_chat_custom_fields_job_spec.rb +++ b/spec/jobs/inboxes/update_widget_pre_chat_custom_fields_job_spec.rb @@ -11,7 +11,9 @@ RSpec.describe Inboxes::UpdateWidgetPreChatCustomFieldsJob do pre_chat_message = 'Share your queries here.' custom_attribute = { '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!(:web_widget) do @@ -23,7 +25,8 @@ RSpec.describe Inboxes::UpdateWidgetPreChatCustomFieldsJob do described_class.perform_now(account, custom_attribute) expect(web_widget.reload.pre_chat_form_options['pre_chat_fields']).to eq [ { '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 diff --git a/spec/mailboxes/imap/imap_mailbox_spec.rb b/spec/mailboxes/imap/imap_mailbox_spec.rb index 798f21112..33302629d 100644 --- a/spec/mailboxes/imap/imap_mailbox_spec.rb +++ b/spec/mailboxes/imap/imap_mailbox_spec.rb @@ -11,7 +11,7 @@ RSpec.describe Imap::ImapMailbox do imap_port: 993, imap_login: 'imap@gmail.com', imap_password: 'password', account: account) end - let(:inbox) { create(:inbox, channel: channel, account: account) } + let(:inbox) { channel.inbox } let!(:contact) { create(:contact, email: 'email@gmail.com', phone_number: '+919584546666', account: account, identifier: '123') } let(:conversation) { Conversation.where(inbox_id: channel.inbox).last } let(:class_instance) { described_class.new } diff --git a/spec/models/inbox_spec.rb b/spec/models/inbox_spec.rb index 30c6ef235..8bcb6bcd5 100644 --- a/spec/models/inbox_spec.rb +++ b/spec/models/inbox_spec.rb @@ -25,7 +25,7 @@ RSpec.describe Inbox do it { is_expected.to have_many(:conversations).dependent(:destroy_async) } - it { is_expected.to have_many(:messages).through(:conversations) } + it { is_expected.to have_many(:messages).dependent(:destroy_async) } it { is_expected.to have_one(:agent_bot_inbox) }