From 666028a443984d925bcbda69e94985385e998ae2 Mon Sep 17 00:00:00 2001
From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
Date: Wed, 29 Dec 2021 18:01:49 +0530
Subject: [PATCH 01/22] feat: Adds multiple path support for fluent icons
(#3665)
* feat: Adds multiple path support for icons
* Changes dashboard icon
---
.../components/FluentIcon/DashboardIcon.vue | 14 ++----
.../shared/components/FluentIcon/Icon.vue | 49 +++++++++++++++++++
.../shared/components/FluentIcon/Index.vue | 18 +++----
3 files changed, 61 insertions(+), 20 deletions(-)
create mode 100644 app/javascript/shared/components/FluentIcon/Icon.vue
diff --git a/app/javascript/shared/components/FluentIcon/DashboardIcon.vue b/app/javascript/shared/components/FluentIcon/DashboardIcon.vue
index c07b9a180..b695aaaa7 100644
--- a/app/javascript/shared/components/FluentIcon/DashboardIcon.vue
+++ b/app/javascript/shared/components/FluentIcon/DashboardIcon.vue
@@ -1,19 +1,15 @@
-
+
{{ additionalAttributes.description }}
@@ -294,19 +301,20 @@ export default { text-align: left; } +.contact--name-wrap { + display: flex; + align-items: center; + margin-bottom: var(--space-small); +} + .contact--name { text-transform: capitalize; white-space: normal; + margin: 0 var(--space-smaller) 0 0; a { color: var(--color-body); } - - .open-link--icon { - color: var(--color-body); - font-size: var(--font-size-small); - margin-left: var(--space-smaller); - } } .contact--metadata { From a61dae0bb29edc7417c52d9528581a974631b422 Mon Sep 17 00:00:00 2001 From: Nayan Patel <79650289+PatelN123@users.noreply.github.com> Date: Wed, 5 Jan 2022 07:07:22 +0000 Subject: [PATCH 07/22] General updates and add CODEOWNERS (#3685) --- CODE_OF_CONDUCT.md | 2 +- CONTRIBUTING.md | 2 +- README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 79a2f8d21..6f64656dd 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -50,7 +50,7 @@ decisions when appropriate. ## Scope -This Code of Conduct applies within all community spaces, and also applies when +This Code of Conduct applies within all community spaces and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bfa510323..935bb293e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,4 +2,4 @@ Thanks for taking the time to contribute! :tada::+1: -Please refer to our [Contributing Guide](https://www.chatwoot.com/docs/contributing-guide) for detailed instructions. +Please refer to our [Contributing Guide](https://www.chatwoot.com/docs/contributing-guide) for detailed instructions on how to contribute. diff --git a/README.md b/README.md index b2e4ccf4f..c3ad01a5e 100644 --- a/README.md +++ b/README.md @@ -118,4 +118,4 @@ Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contriCanned Responses
Canned Responses are saved reply templates which can be used to quickly send out a reply to a conversation .
For creating a Canned Response, just click on the Add Canned Response. You can also edit or delete an existing Canned Response by clicking on the Edit or Delete button
Canned responses are used with the help of Short Codes. Agents can access canned responses while on a chat by typing '/' followed by the short code.
", + "SIDEBAR_TXT": "Canned Responses
Canned Responses are saved reply templates which can be used to quickly send out a reply to a conversation.
For creating a Canned Response, just click on the Add Canned Response. You can also edit or delete an existing Canned Response by clicking on the Edit or Delete button
Canned responses are used with the help of Short Codes. Agents can access canned responses while on a chat by typing '/' followed by the short code.
", "LIST": { "404": "There are no canned responses available in this account.", "TITLE": "Manage canned responses", From a0884310f4db43872f02f4050e79ddf4ffbb2b35 Mon Sep 17 00:00:00 2001 From: Tejaswini ChileHi {{user.available_name}}
+ + +Time to save the world. A new conversation has been created in {{ inbox.name }}
+ ++Click here to get cracking. +
diff --git a/config/routes.rb b/config/routes.rb index 47835f1bf..493810c03 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -52,6 +52,7 @@ Rails.application.routes.draw do end end resources :canned_responses, except: [:show, :edit, :new] + resources :automation_rules, only: [:create, :index] resources :campaigns, only: [:index, :create, :show, :update, :destroy] namespace :channels do diff --git a/db/migrate/20211110101046_create_automation_rules.rb b/db/migrate/20211110101046_create_automation_rules.rb new file mode 100644 index 000000000..ee7349413 --- /dev/null +++ b/db/migrate/20211110101046_create_automation_rules.rb @@ -0,0 +1,14 @@ +class CreateAutomationRules < ActiveRecord::Migration[6.1] + def change + create_table :automation_rules do |t| + t.bigint :account_id, null: false + t.string :name, null: false + t.text :description + t.string :event_name, null: false + t.jsonb :conditions, null: false, default: '{}' + t.jsonb :actions, null: false, default: '{}' + t.timestamps + t.index :account_id, name: 'index_automation_rules_on_account_id' + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 1429612cd..14b64b78a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -124,6 +124,18 @@ ActiveRecord::Schema.define(version: 2021_12_21_125545) do t.string "extension" end + create_table "automation_rules", force: :cascade do |t| + t.bigint "account_id", null: false + t.string "name", null: false + t.text "description" + t.string "event_name", null: false + t.jsonb "conditions", default: "{}", null: false + t.jsonb "actions", default: "{}", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["account_id"], name: "index_automation_rules_on_account_id" + end + create_table "campaigns", force: :cascade do |t| t.integer "display_id", null: false t.string "title", null: false diff --git a/lib/automation_rules/conditions.json b/lib/automation_rules/conditions.json new file mode 100644 index 000000000..ad96b5289 --- /dev/null +++ b/lib/automation_rules/conditions.json @@ -0,0 +1,160 @@ +{ + "conversations": { + "status": { + "attribute_name": "Status", + "input_type": "multi_select", + "table_name": "conversations", + "filter_operators": [ "equal_to", "not_equal_to" ], + "attribute_type": "standard" + }, + "assignee_id": { + "attribute_name": "Assignee Name", + "input_type": "search_box with name tags/plain text", + "table_name": "conversations", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ], + "attribute_type": "standard" + }, + "contact_id": { + "attribute_name": "Contact Name", + "input_type": "plain_text", + "table_name": "conversations", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ], + "attribute_type": "standard" + }, + "inbox_id": { + "attribute_name": "Inbox Name", + "input_type": "search_box", + "table_name": "conversations", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ], + "attribute_type": "standard" + }, + "team_id": { + "attribute_name": "Team Name", + "input_type": "search_box", + "table_name": "conversations", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ], + "attribute_type": "standard" + }, + "id": { + "attribute_name": "Conversation Identifier", + "input_type": "textbox", + "table_name": "conversations", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ], + "attribute_type": "standard" + }, + "campaign_id": { + "attribute_name": "Campaign Name", + "input_type": "textbox", + "data_type": "Number", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ], + "attribute_type": "standard" + }, + "labels": { + "attribute_name": "Labels", + "input_type": "tags", + "data_type": "text", + "filter_operators": ["exactly_equal_to", "contains", "does_not_contain" ], + "attribute_type": "standard" + }, + "browser_language": { + "attribute_name": "Browser Language", + "input_type": "textbox", + "data_type": "text", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain" ], + "attribute_type": "additional_attributes" + }, + "country_code": { + "attribute_name": "Country Name", + "input_type": "textbox", + "data_type": "text", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "present", "is_not_present" ], + "attribute_type": "additional_attributes" + }, + "referer": { + "attribute_name": "Referer link", + "input_type": "textbox", + "data_type": "link", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "present", "is_not_present" ], + "attribute_type": "additional_attributes" + }, + "plan": { + "attribute_name": "Plan", + "input_type": "multi_select", + "data_type": "text", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "present", "is_not_present" ], + "attribute_type": "additional_attributes" + } + }, + "contacts": { + "assignee_id": { + "attribute_name": "Assignee Name", + "input_type": "search_box with name tags/plain text", + "data_type": "text", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ], + "attribute_type": "standard" + }, + "contact_id": { + "attribute_name": "Contact Name", + "input_type": "plain_text", + "data_type": "text", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ], + "attribute_type": "standard" + }, + "inbox_id": { + "attribute_name": "Inbox Name", + "input_type": "search_box", + "data_type": "text", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ], + "attribute_type": "standard" + }, + "team_id": { + "attribute_name": "Team Name", + "input_type": "search_box", + "data_type": "number", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ], + "attribute_type": "standard" + }, + "id": { + "attribute_name": "Conversation Identifier", + "input_type": "textbox", + "data_type": "Number", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ], + "attribute_type": "standard" + }, + "campaign_id": { + "attribute_name": "Campaign Name", + "input_type": "textbox", + "data_type": "Number", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ], + "attribute_type": "standard" + }, + "labels": { + "attribute_name": "Labels", + "input_type": "tags", + "data_type": "text", + "filter_operators": ["exactly_equal_to", "contains", "does_not_contain" ], + "attribute_type": "standard" + }, + "browser_language": { + "attribute_name": "Browser Language", + "input_type": "textbox", + "data_type": "text", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain" ], + "attribute_type": "additional_attributes" + }, + "country_code": { + "attribute_name": "Country Name", + "input_type": "textbox", + "data_type": "text", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "present", "is_not_present" ], + "attribute_type": "additional_attributes" + }, + "referer": { + "attribute_name": "Referer link", + "input_type": "textbox", + "data_type": "link", + "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "present", "is_not_present" ], + "attribute_type": "additional_attributes" + } + } +} diff --git a/spec/controllers/api/v1/accounts/automation_rules_controller_spec.rb b/spec/controllers/api/v1/accounts/automation_rules_controller_spec.rb new file mode 100644 index 000000000..d922c7f06 --- /dev/null +++ b/spec/controllers/api/v1/accounts/automation_rules_controller_spec.rb @@ -0,0 +1,118 @@ +require 'rails_helper' + +RSpec.describe 'Api::V1::Accounts::AutomationRulesController', type: :request do + let(:account) { create(:account) } + let(:administrator) { create(:user, account: account, role: :administrator) } + let!(:inbox) { create(:inbox, account: account, enable_auto_assignment: false) } + let!(:contact) { create(:contact, account: account) } + let(:contact_inbox) { create(:contact_inbox, inbox_id: inbox.id, contact_id: contact.id) } + + describe 'GET /api/v1/accounts/{account.id}/automation_rules' do + context 'when it is an authenticated user' do + it 'returns all records' do + automation_rule = create(:automation_rule, account: account, name: 'Test Automation Rule') + + get "/api/v1/accounts/#{account.id}/automation_rules", + headers: administrator.create_new_auth_token + + expect(response).to have_http_status(:success) + body = JSON.parse(response.body, symbolize_names: true) + expect(body[:data].first[:id]).to eq(automation_rule.id) + end + end + + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/automation_rules" + + expect(response).to have_http_status(:unauthorized) + end + end + end + + describe 'POST /api/v1/accounts/{account.id}/automation_rules' do + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/automation_rules" + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + let(:params) do + { + name: 'Notify Conversation Created and mark priority query', + description: 'Notify all administrator about conversation created and mark priority query', + event_name: 'conversation_created', + conditions: [ + { + attribute_key: 'browser_language', + filter_operator: 'equal_to', + values: ['en'], + query_operator: 'AND' + }, + { + attribute_key: 'country_code', + filter_operator: 'equal_to', + values: %w[USA UK], + query_operator: nil + } + ], + actions: [ + { + action_name: :send_message, + action_params: ['Welcome to the chatwoot platform.'] + }, + { + action_name: :assign_team, + action_params: [1] + }, + { + action_name: :add_label, + action_params: %w[support priority_customer] + }, + { + action_name: :assign_best_administrator, + action_params: [1] + }, + { + action_name: :update_additional_attributes, + action_params: [{ intiated_at: '2021-12-03 17:25:26.844536 +0530' }] + } + ] + }.with_indifferent_access + end + + it 'Saves for automation_rules for account with country_code and browser_language conditions' do + expect(account.automation_rules.count).to eq(0) + + post "/api/v1/accounts/#{account.id}/automation_rules", + headers: administrator.create_new_auth_token, + params: params + + expect(response).to have_http_status(:success) + expect(account.automation_rules.count).to eq(1) + end + + it 'Saves for automation_rules for account with status conditions' do + params[:conditions] = [ + { + attribute_key: 'status', + filter_operator: 'equal_to', + values: ['resolved'], + query_operator: nil + }.with_indifferent_access + ] + expect(account.automation_rules.count).to eq(0) + + post "/api/v1/accounts/#{account.id}/automation_rules", + headers: administrator.create_new_auth_token, + params: params + + expect(response).to have_http_status(:success) + expect(account.automation_rules.count).to eq(1) + end + end + end +end diff --git a/spec/factories/automation_rules.rb b/spec/factories/automation_rules.rb new file mode 100644 index 000000000..1b37e40d2 --- /dev/null +++ b/spec/factories/automation_rules.rb @@ -0,0 +1,19 @@ +FactoryBot.define do + factory :automation_rule do + account + event_name { 'conversation_status_changed' } + conditions { [{ 'values': ['resolved'], 'attribute_key': 'status', 'query_operator': nil, 'filter_operator': 'equal_to' }] } + actions do + [ + { + 'action_name' => 'send_email_to_team', 'action_params' => { + 'message' => 'Please pay attention to this conversation, its from high priority customer', 'team_ids' => [1] + } + }, + { 'action_name' => 'assign_team', 'action_params' => [1] }, + { 'action_name' => 'add_label', 'action_params' => %w[support priority_customer] }, + { 'action_name' => 'assign_best_agents', 'action_params' => [1, 2, 3, 4] } + ] + end + end +end diff --git a/spec/listeners/automation_rule_listener_spec.rb b/spec/listeners/automation_rule_listener_spec.rb new file mode 100644 index 000000000..f14167f5f --- /dev/null +++ b/spec/listeners/automation_rule_listener_spec.rb @@ -0,0 +1,72 @@ +require 'rails_helper' +describe AutomationRuleListener do + let(:listener) { described_class.instance } + let(:account) { create(:account) } + let(:inbox) { create(:inbox, account: account) } + let(:contact) { create(:contact, account: account, identifier: '123') } + let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: inbox) } + let(:conversation) { create(:conversation, contact_inbox: contact_inbox, inbox: inbox, account: account) } + let(:automation_rule) { create(:automation_rule, account: account, name: 'Test Automation Rule') } + let(:team) { create(:team, account: account) } + let(:user_1) { create(:user, role: 0) } + let(:user_2) { create(:user, role: 0) } + let!(:event) do + Events::Base.new('conversation_status_changed', Time.zone.now, { conversation: conversation }) + end + + before do + create(:team_member, user: user_1, team: team) + create(:team_member, user: user_2, team: team) + create(:account_user, user: user_2, account: account) + create(:account_user, user: user_1, account: account) + + conversation.resolved! + automation_rule.update!(actions: + [ + { + 'action_name' => 'send_email_to_team', 'action_params' => { + 'message' => 'Please pay attention to this conversation, its from high priority customer', + 'team_ids' => [team.id] + } + }, + { 'action_name' => 'assign_team', 'action_params' => [team.id] }, + { 'action_name' => 'add_label', 'action_params' => %w[support priority_customer] }, + { 'action_name' => 'assign_best_agents', 'action_params' => [user_1.id] } + ]) + end + + describe '#conversation_status_changed' do + context 'when rule matches' do + it 'triggers automation rule to assign team' do + expect(conversation.team_id).not_to eq(team.id) + + automation_rule + listener.conversation_status_changed(event) + + conversation.reload + expect(conversation.team_id).to eq(team.id) + end + + it 'triggers automation rule to add label' do + expect(conversation.labels).to eq([]) + + automation_rule + listener.conversation_status_changed(event) + + conversation.reload + expect(conversation.labels.pluck(:name)).to eq(%w[support priority_customer]) + end + + it 'triggers automation rule to assign best agents' do + expect(conversation.assignee).to be_nil + + automation_rule + listener.conversation_status_changed(event) + + conversation.reload + + expect(conversation.assignee).to eq(user_1) + end + end + end +end diff --git a/spec/models/automation_rule_spec.rb b/spec/models/automation_rule_spec.rb new file mode 100644 index 000000000..6b06c3197 --- /dev/null +++ b/spec/models/automation_rule_spec.rb @@ -0,0 +1,54 @@ +require 'rails_helper' + +RSpec.describe AutomationRule, type: :model do + describe 'associations' do + let(:params) do + { + name: 'Notify Conversation Created and mark priority query', + description: 'Notify all administrator about conversation created and mark priority query', + event_name: 'conversation_created', + conditions: [ + { + attribute_key: 'browser_language', + filter_operator: 'equal_to', + values: ['en'], + query_operator: 'AND' + }, + { + attribute_key: 'country_code', + filter_operator: 'equal_to', + values: %w[USA UK], + query_operator: nil + } + ], + actions: [ + { + action_name: :send_message, + action_params: ['Welcome to the chatwoot platform.'] + }, + { + action_name: :assign_team, + action_params: [1] + }, + { + action_name: :add_label, + action_params: %w[support priority_customer] + }, + { + action_name: :assign_best_administrator, + action_params: [1] + }, + { + action_name: :update_additional_attributes, + action_params: [{ intiated_at: '2021-12-03 17:25:26.844536 +0530' }] + } + ] + }.with_indifferent_access + end + + it 'returns valid record' do + rule = FactoryBot.build(:automation_rule, params) + expect(rule.valid?).to eq true + end + end +end From 7ee70628433185407eb3558e5fbf8230aa372ae3 Mon Sep 17 00:00:00 2001 From: "Aswin Dev P.S"