diff --git a/app/javascript/dashboard/composables/spec/useAutomation.spec.js b/app/javascript/dashboard/composables/spec/useAutomation.spec.js index 6cce15996..2e07f1671 100644 --- a/app/javascript/dashboard/composables/spec/useAutomation.spec.js +++ b/app/javascript/dashboard/composables/spec/useAutomation.spec.js @@ -8,6 +8,7 @@ import { agents, teams, labels, + booleanFilterOptions, statusFilterOptions, messageTypeOptions, priorityOptions, @@ -73,6 +74,8 @@ describe('useAutomation', () => { return countries; case 'message_type': return messageTypeOptions; + case 'private_note': + return booleanFilterOptions; case 'priority': return priorityOptions; default: @@ -226,6 +229,9 @@ describe('useAutomation', () => { expect(getConditionDropdownValues('message_type')).toEqual( messageTypeOptions ); + expect(getConditionDropdownValues('private_note')).toEqual( + booleanFilterOptions + ); expect(getConditionDropdownValues('priority')).toEqual(priorityOptions); }); diff --git a/app/javascript/dashboard/composables/spec/useEditableAutomation.spec.js b/app/javascript/dashboard/composables/spec/useEditableAutomation.spec.js new file mode 100644 index 000000000..c6177e9a2 --- /dev/null +++ b/app/javascript/dashboard/composables/spec/useEditableAutomation.spec.js @@ -0,0 +1,54 @@ +import { useEditableAutomation } from '../useEditableAutomation'; +import useAutomationValues from '../useAutomationValues'; + +vi.mock('../useAutomationValues'); + +describe('useEditableAutomation', () => { + beforeEach(() => { + useAutomationValues.mockReturnValue({ + getConditionDropdownValues: vi.fn(attributeKey => { + if (attributeKey === 'private_note') { + return [ + { id: true, name: 'True' }, + { id: false, name: 'False' }, + ]; + } + + return []; + }), + getActionDropdownValues: vi.fn(), + }); + }); + + it('rehydrates boolean conditions as a single selected option', () => { + const automation = { + event_name: 'message_created', + conditions: [ + { + attribute_key: 'private_note', + filter_operator: 'equal_to', + values: [false], + query_operator: null, + }, + ], + actions: [], + }; + const automationTypes = { + message_created: { + conditions: [{ key: 'private_note', inputType: 'search_select' }], + }, + }; + + const { formatAutomation } = useEditableAutomation(); + const result = formatAutomation(automation, [], automationTypes, []); + + expect(result.conditions).toEqual([ + { + attribute_key: 'private_note', + filter_operator: 'equal_to', + values: { id: false, name: 'False' }, + query_operator: 'and', + }, + ]); + }); +}); diff --git a/app/javascript/dashboard/composables/useEditableAutomation.js b/app/javascript/dashboard/composables/useEditableAutomation.js index 8b9041a8f..3f4e65b3c 100644 --- a/app/javascript/dashboard/composables/useEditableAutomation.js +++ b/app/javascript/dashboard/composables/useEditableAutomation.js @@ -46,11 +46,26 @@ export function useEditableAutomation() { if (inputType === 'comma_separated_plain_text') { return { ...condition, values: condition.values.join(',') }; } + const dropdownValues = getConditionDropdownValues( + condition.attribute_key + ); + const hasBooleanOptions = + inputType === 'search_select' && + dropdownValues.length && + dropdownValues.every(item => typeof item.id === 'boolean'); + + if (hasBooleanOptions) { + return { + ...condition, + query_operator: condition.query_operator || 'and', + values: dropdownValues.find(item => item.id === condition.values[0]), + }; + } return { ...condition, query_operator: condition.query_operator || 'and', - values: [...getConditionDropdownValues(condition.attribute_key)].filter( - item => [...condition.values].includes(item.id) + values: [...dropdownValues].filter(item => + [...condition.values].includes(item.id) ), }; }); diff --git a/app/javascript/dashboard/helper/automationHelper.js b/app/javascript/dashboard/helper/automationHelper.js index fa6120c16..8aed8dcda 100644 --- a/app/javascript/dashboard/helper/automationHelper.js +++ b/app/javascript/dashboard/helper/automationHelper.js @@ -150,6 +150,7 @@ export const getConditionOptions = ({ conversation_language: languages, country_code: countries, message_type: messageTypeOptions, + private_note: booleanFilterOptions, priority: priorityOptions, labels: generateConditionOptions(labels, 'title'), }; diff --git a/app/javascript/dashboard/helper/specs/automationHelper.spec.js b/app/javascript/dashboard/helper/specs/automationHelper.spec.js index 10088963a..033481a0b 100644 --- a/app/javascript/dashboard/helper/specs/automationHelper.spec.js +++ b/app/javascript/dashboard/helper/specs/automationHelper.spec.js @@ -178,6 +178,21 @@ describe('getConditionOptions', () => { }) ).toEqual(testOptions); }); + + it('returns boolean options for private_note', () => { + const booleanOptions = [ + { id: true, name: 'True' }, + { id: false, name: 'False' }, + ]; + + expect( + helpers.getConditionOptions({ + booleanFilterOptions: booleanOptions, + customAttributes, + type: 'private_note', + }) + ).toEqual(booleanOptions); + }); }); describe('getFileName', () => { diff --git a/app/javascript/dashboard/i18n/locale/en/automation.json b/app/javascript/dashboard/i18n/locale/en/automation.json index 22a9735f4..d338fa9a2 100644 --- a/app/javascript/dashboard/i18n/locale/en/automation.json +++ b/app/javascript/dashboard/i18n/locale/en/automation.json @@ -169,6 +169,7 @@ }, "ATTRIBUTES": { "MESSAGE_TYPE": "Message Type", + "PRIVATE_NOTE": "Private Note", "MESSAGE_CONTAINS": "Message Contains", "EMAIL": "Email", "INBOX": "Inbox", diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js b/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js index 3acca3e2e..24947c63b 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js +++ b/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js @@ -14,6 +14,12 @@ export const AUTOMATIONS = { inputType: 'search_select', filterOperators: OPERATOR_TYPES_1, }, + { + key: 'private_note', + name: 'PRIVATE_NOTE', + inputType: 'search_select', + filterOperators: OPERATOR_TYPES_1, + }, { key: 'content', name: 'MESSAGE_CONTAINS', diff --git a/app/models/automation_rule.rb b/app/models/automation_rule.rb index 8162abb91..3ab23530d 100644 --- a/app/models/automation_rule.rb +++ b/app/models/automation_rule.rb @@ -36,7 +36,7 @@ class AutomationRule < ApplicationRecord def conditions_attributes %w[content email country_code status message_type browser_language assignee_id team_id referer city company inbox_id - mail_subject phone_number priority conversation_language labels] + mail_subject phone_number priority conversation_language labels private_note] end def actions_attributes diff --git a/app/services/automation_rules/conditions_filter_service.rb b/app/services/automation_rules/conditions_filter_service.rb index 993ed21c9..862faceac 100644 --- a/app/services/automation_rules/conditions_filter_service.rb +++ b/app/services/automation_rules/conditions_filter_service.rb @@ -113,6 +113,7 @@ class AutomationRules::ConditionsFilterService < FilterService query_operator = query_hash['query_operator'] attribute_key = 'processed_message_content' if attribute_key == 'content' + attribute_key = 'private' if attribute_key == 'private_note' filter_operator_value = filter_operation(query_hash, current_index) diff --git a/lib/filters/filter_keys.yml b/lib/filters/filter_keys.yml index bfaf39325..8711239cc 100644 --- a/lib/filters/filter_keys.yml +++ b/lib/filters/filter_keys.yml @@ -214,6 +214,12 @@ messages: filter_operators: - "equal_to" - "not_equal_to" + private_note: + attribute_type: "standard" + data_type: "boolean" + filter_operators: + - "equal_to" + - "not_equal_to" content: attribute_type: "standard" data_type: "text" diff --git a/spec/listeners/automation_rule_listener_spec.rb b/spec/listeners/automation_rule_listener_spec.rb index 57a096a10..08085da7a 100644 --- a/spec/listeners/automation_rule_listener_spec.rb +++ b/spec/listeners/automation_rule_listener_spec.rb @@ -220,6 +220,15 @@ describe AutomationRuleListener do expect(AutomationRules::ActionService).not_to have_received(:new) end + it 'calls AutomationRules::ActionService if message is a private note' do + message.update!(private: true) + allow(condition_match).to receive(:present?).and_return(true) + + listener.message_created(event) + + expect(AutomationRules::ActionService).to have_received(:new).with(automation_rule, account, conversation) + end + it 'does not call AutomationRules::ActionService if conditions do not match based on content' do message.update!(processed_message_content: 'hi', content: "hi\n\nhello") allow(condition_match).to receive(:present?).and_return(false) diff --git a/spec/models/automation_rule_spec.rb b/spec/models/automation_rule_spec.rb index 91452b8a4..cd6297713 100644 --- a/spec/models/automation_rule_spec.rb +++ b/spec/models/automation_rule_spec.rb @@ -86,6 +86,19 @@ RSpec.describe AutomationRule do rule = FactoryBot.build(:automation_rule, params) expect(rule.valid?).to be true end + + it 'allows private_note as a valid condition attribute' do + params[:conditions] = [ + { + attribute_key: 'private_note', + filter_operator: 'equal_to', + values: [true], + query_operator: nil + } + ] + rule = FactoryBot.build(:automation_rule, params) + expect(rule.valid?).to be true + end end describe 'reauthorizable' do diff --git a/spec/services/automation_rules/condition_validation_service_spec.rb b/spec/services/automation_rules/condition_validation_service_spec.rb index 36387754a..1f65fb475 100644 --- a/spec/services/automation_rules/condition_validation_service_spec.rb +++ b/spec/services/automation_rules/condition_validation_service_spec.rb @@ -10,7 +10,8 @@ RSpec.describe AutomationRules::ConditionValidationService do rule.conditions = [ { 'values': ['open'], 'attribute_key': 'status', 'query_operator': nil, 'filter_operator': 'equal_to' }, { 'values': ['+918484'], 'attribute_key': 'phone_number', 'query_operator': 'OR', 'filter_operator': 'contains' }, - { 'values': ['test'], 'attribute_key': 'email', 'query_operator': nil, 'filter_operator': 'contains' } + { 'values': ['test'], 'attribute_key': 'email', 'query_operator': 'OR', 'filter_operator': 'contains' }, + { 'values': [true], 'attribute_key': 'private_note', 'query_operator': nil, 'filter_operator': 'equal_to' } ] rule.save end diff --git a/spec/services/automation_rules/conditions_filter_service_spec.rb b/spec/services/automation_rules/conditions_filter_service_spec.rb index 426cb533e..c4ff81275 100644 --- a/spec/services/automation_rules/conditions_filter_service_spec.rb +++ b/spec/services/automation_rules/conditions_filter_service_spec.rb @@ -83,6 +83,27 @@ RSpec.describe AutomationRules::ConditionsFilterService do end end + context 'when filtering private notes' do + before do + rule.conditions = [ + { 'values': [true], 'attribute_key': 'private_note', 'query_operator': nil, 'filter_operator': 'equal_to' } + ] + rule.save + end + + it 'will return true when the message is a private note' do + message.update!(private: true) + + expect(described_class.new(rule, conversation, { message: message, changed_attributes: {} }).perform).to be(true) + end + + it 'will return false when the message is not a private note' do + message.update!(private: false) + + expect(described_class.new(rule, conversation, { message: message, changed_attributes: {} }).perform).to be(false) + end + end + context 'when filter_operator is on processed_message_content' do before do rule.conditions = [