Merge branch 'develop' into fix/editor-heading-error

This commit is contained in:
Sivin Varghese
2024-01-24 11:09:20 +05:30
committed by GitHub
38 changed files with 395 additions and 58 deletions
@@ -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
@@ -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() {
@@ -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`
"
>
@@ -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,
});
@@ -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',
},
],
};
@@ -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',
},
]);
});
});
});
@@ -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"
}
}
}
@@ -339,7 +339,8 @@
},
"VALIDATIONS": {
"REQUIRED": "Valid value is required",
"INVALID_URL": "Invalid URL"
"INVALID_URL": "Invalid URL",
"INVALID_INPUT": "Invalid Input"
}
},
"MERGE_CONTACTS": {
@@ -324,7 +324,8 @@
"LAST_EDITED": "Last edited"
},
"COLUMNS": {
"BY": "by"
"BY": "by",
"AUTHOR_NOT_AVAILABLE": "Author is not available"
}
},
"EDIT_ARTICLE": {
@@ -0,0 +1,11 @@
export default {
methods: {
getRegexp(regexPatternValue) {
let lastSlash = regexPatternValue.lastIndexOf('/');
return new RegExp(
regexPatternValue.slice(1, lastSlash),
regexPatternValue.slice(lastSlash + 1)
);
},
},
};
@@ -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"
@@ -11,9 +11,30 @@
{{ title }}
</h6>
</router-link>
<div class="author">
<span class="by">{{ $t('HELP_CENTER.TABLE.COLUMNS.BY') }}</span>
<span class="name">{{ articleAuthorName }}</span>
<div class="flex gap-1 items-center">
<Thumbnail
v-if="author"
:src="author.thumbnail"
:username="author.name"
size="16px"
/>
<div
v-else
v-tooltip.right="
$t('HELP_CENTER.TABLE.COLUMNS.AUTHOR_NOT_AVAILABLE')
"
class="flex items-center justify-center rounded w-4 h-4 bg-woot-100 dark:bg-woot-700"
>
<fluent-icon
icon="person"
type="filled"
size="10"
class="text-woot-300 dark:text-woot-300"
/>
</div>
<span class="font-normal text-slate-500 dark:text-slate-200 text-sm">
{{ articleAuthorName }}
</span>
</div>
</div>
</span>
@@ -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 {
@@ -86,6 +86,30 @@
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.ERROR') }}
</label>
</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">
<woot-submit-button
:disabled="isButtonDisabled"
@@ -124,6 +148,9 @@ export default {
attributeModel: 0,
attributeType: 0,
attributeKey: '',
regexPattern: null,
regexCue: null,
regexEnabled: false,
models: ATTRIBUTE_MODELS,
types: ATTRIBUTE_TYPES,
values: [],
@@ -163,6 +190,12 @@ export default {
isAttributeTypeList() {
return this.attributeType === 6;
},
isAttributeTypeText() {
return this.attributeType === 0;
},
isRegexEnabled() {
return this.regexEnabled;
},
},
validations: {
@@ -201,11 +234,18 @@ export default {
onDisplayNameChange() {
this.attributeKey = convertToAttributeSlug(this.displayName);
},
toggleRegexEnabled() {
this.regexEnabled = !this.regexEnabled;
},
async addAttributes() {
this.$v.$touch();
if (this.$v.$invalid) {
return;
}
if (!this.regexEnabled) {
this.regexPattern = null;
this.regexCue = null;
}
try {
await this.$store.dispatch('attributes/create', {
attribute_display_name: this.displayName,
@@ -214,6 +254,10 @@ export default {
attribute_display_type: this.attributeType,
attribute_key: this.attributeKey,
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.onClose();
@@ -70,6 +70,30 @@
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.ERROR') }}
</label>
</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 class="flex flex-row justify-end gap-2 py-2 px-0 w-full">
<woot-button :is-loading="isUpdating" :disabled="isButtonDisabled">
@@ -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;
},
},
};
</script>
@@ -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",
@@ -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') {
+2 -1
View File
@@ -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": {
@@ -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!
+1 -1
View File
@@ -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
@@ -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
+1 -1
View File
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
+19 -3
View File
@@ -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
@@ -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
+23
View File
@@ -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
+10 -8
View File
@@ -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
#
@@ -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
@@ -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
@@ -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
@@ -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
+8
View File
@@ -0,0 +1,8 @@
FactoryBot.define do
factory :applied_sla do
account
sla_policy
conversation
sla_status { 'active' }
end
end
+4 -2
View File
@@ -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
@@ -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
+1 -1
View File
@@ -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 }
+1 -1
View File
@@ -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) }