Date: Tue, 23 Jan 2024 19:31:57 +0530
Subject: [PATCH 3/4] 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
---
...custom_attribute_definitions_controller.rb | 2 +
.../dashboard/components/CustomAttribute.vue | 23 +++++++-
app/javascript/dashboard/helper/preChat.js | 2 +
.../dashboard/helper/specs/inboxFixture.js | 14 +++++
.../dashboard/helper/specs/preChat.spec.js | 24 +++++++-
.../i18n/locale/en/attributesMgmt.json | 22 ++++++++
.../dashboard/i18n/locale/en/contact.json | 3 +-
.../dashboard/mixins/customAttributeMixin.js | 11 ++++
.../customAttributes/CustomAttributes.vue | 2 +
.../settings/attributes/AddAttribute.vue | 44 +++++++++++++++
.../settings/attributes/EditAttribute.vue | 55 ++++++++++++++++++-
.../widget/components/PreChat/Form.vue | 39 +++++++++----
app/javascript/widget/i18n/locale/en.json | 3 +-
...pdate_widget_pre_chat_custom_fields_job.rb | 4 +-
app/models/channel/web_widget.rb | 2 +-
app/models/custom_attribute_definition.rb | 2 +
..._custom_attribute_definition.json.jbuilder | 2 +
...dd_regex_to_custom_attribute_definition.rb | 6 ++
db/schema.rb | 2 +
..._widget_pre_chat_custom_fields_job_spec.rb | 7 ++-
20 files changed, 247 insertions(+), 22 deletions(-)
create mode 100644 app/javascript/dashboard/mixins/customAttributeMixin.js
create mode 100644 db/migrate/20230905060223_add_regex_to_custom_attribute_definition.rb
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/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/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/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/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/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/schema.rb b/db/schema.rb
index 1fdf26882..2d7debe09 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -501,6 +501,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
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
From 232369cd5c0b86e6afae43cd7f552ff93b111b8f Mon Sep 17 00:00:00 2001
From: Vishnu Narayanan
Date: Tue, 23 Jan 2024 23:48:02 +0530
Subject: [PATCH 4/4] feat: sla 1 - refactor sla_policies model and add
applied_sla model (#8602)
* feat: add models
* chore: refactor sla column names
* chore: remove foreign keys
* chore: fix spec
* chore: refactor models
---
...31223033019_refactor_sla_policy_columns.rb | 8 +++++++
.../20231223040257_create_applied_slas.rb | 13 +++++++++++
db/schema.rb | 20 +++++++++++++---
.../v1/accounts/sla_policies_controller.rb | 3 ++-
enterprise/app/models/applied_sla.rb | 23 +++++++++++++++++++
enterprise/app/models/sla_policy.rb | 18 ++++++++-------
.../api/v1/models/_sla_policy.json.jbuilder | 6 +++--
.../accounts/sla_policies_controller_spec.rb | 6 +++--
spec/enterprise/models/applied_sla_spec.rb | 16 +++++++++++++
spec/enterprise/models/sla_policy_spec.rb | 5 ++++
spec/factories/applied_slas.rb | 8 +++++++
spec/factories/sla_policies.rb | 6 +++--
12 files changed, 114 insertions(+), 18 deletions(-)
create mode 100644 db/migrate/20231223033019_refactor_sla_policy_columns.rb
create mode 100644 db/migrate/20231223040257_create_applied_slas.rb
create mode 100644 enterprise/app/models/applied_sla.rb
create mode 100644 spec/enterprise/models/applied_sla_spec.rb
create mode 100644 spec/factories/applied_slas.rb
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 2d7debe09..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
@@ -824,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