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 @@ diff --git a/app/javascript/shared/components/FluentIcon/Index.vue b/app/javascript/shared/components/FluentIcon/Index.vue index 6b108ff52..7bc98a7a7 100644 --- a/app/javascript/shared/components/FluentIcon/Index.vue +++ b/app/javascript/shared/components/FluentIcon/Index.vue @@ -1,27 +1,23 @@ diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue index 037cf1e73..d2cf8974a 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue @@ -10,17 +10,24 @@ />
-

+
+

+ {{ contact.name }} +

- {{ contact.name }} - + -

+

{{ 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/contri -*Chatwoot* © 2017-2021, Chatwoot Inc - Released under the MIT License. +*Chatwoot* © 2017-2022, Chatwoot Inc - Released under the MIT License. From e1fe4f09bc6c2a6d4e6c0be85b1cb2d697844424 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Thu, 6 Jan 2022 21:40:43 +0530 Subject: [PATCH 08/22] chore: Add subscription delete API (#3703) --- .../notification_subscriptions_controller.rb | 6 ++++ config/routes.rb | 2 +- ...ification_subscriptions_controller_spec.rb | 28 +++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/v1/notification_subscriptions_controller.rb b/app/controllers/api/v1/notification_subscriptions_controller.rb index 98ff9ea7a..5f1cf30e4 100644 --- a/app/controllers/api/v1/notification_subscriptions_controller.rb +++ b/app/controllers/api/v1/notification_subscriptions_controller.rb @@ -7,6 +7,12 @@ class Api::V1::NotificationSubscriptionsController < Api::BaseController render json: notification_subscription end + def destroy + notification_subscription = NotificationSubscription.where(["subscription_attributes->>'push_token' = ?", params[:push_token]]).first + notification_subscription.destroy + head :ok + end + private def set_user diff --git a/config/routes.rb b/config/routes.rb index 135e15a04..47835f1bf 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -172,7 +172,7 @@ Rails.application.routes.draw do end end - resource :notification_subscriptions, only: [:create] + resource :notification_subscriptions, only: [:create, :destroy] namespace :widget do resource :config, only: [:create] diff --git a/spec/controllers/api/v1/notification_subscriptions_controller_spec.rb b/spec/controllers/api/v1/notification_subscriptions_controller_spec.rb index 3a476fd20..942153dfd 100644 --- a/spec/controllers/api/v1/notification_subscriptions_controller_spec.rb +++ b/spec/controllers/api/v1/notification_subscriptions_controller_spec.rb @@ -80,4 +80,32 @@ RSpec.describe 'Notifications Subscriptions API', type: :request do end end end + + describe 'DELETE /api/v1/notification_subscriptions' do + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + delete '/api/v1/notification_subscriptions' + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + let(:agent) { create(:user, account: account, role: :agent) } + + it 'delete existing notification subscription if subscription exists' do + subscription = create(:notification_subscription, subscription_type: 'fcm', subscription_attributes: { push_token: 'bUvZo8AYGGmCMr' }, + user: agent) + delete '/api/v1/notification_subscriptions', + params: { + push_token: subscription.subscription_attributes['push_token'] + }, + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + expect { subscription.reload }.to raise_exception(ActiveRecord::RecordNotFound) + end + end + end end From f7ed7756d99cf345f33eb6e1495534bf8a702bca Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Thu, 6 Jan 2022 15:09:01 -0800 Subject: [PATCH 09/22] chore: Use logo thumbnail instead of logo in the sidebar (#3706) --- .../dashboard/components/layout/Sidebar.vue | 2 +- public/favicon.ico | Bin 1150 -> 0 bytes 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 public/favicon.ico diff --git a/app/javascript/dashboard/components/layout/Sidebar.vue b/app/javascript/dashboard/components/layout/Sidebar.vue index 2a8cca7c4..cd0d0a08e 100644 --- a/app/javascript/dashboard/components/layout/Sidebar.vue +++ b/app/javascript/dashboard/components/layout/Sidebar.vue @@ -1,7 +1,7 @@ diff --git a/package.json b/package.json index 07747ed94..13f3ee313 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "lodash.groupby": "^4.6.0", "marked": "2.0.3", "md5": "^2.3.0", - "ninja-keys": "https://github.com/chatwoot/ninja-keys.git#b4c3233f676780af90c607866fa85e404c835902", + "ninja-keys": "^1.1.9", "posthog-js": "^1.13.7", "prosemirror-markdown": "1.5.1", "prosemirror-state": "1.3.4", diff --git a/yarn.lock b/yarn.lock index 0255cb0d3..9fcc42b62 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1511,10 +1511,10 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" -"@lit/reactive-element@^1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.0.1.tgz#853cacd4d78d79059f33f66f8e7b0e5c34bee294" - integrity sha512-nSD5AA2AZkKuXuvGs8IK7K5ZczLAogfDd26zT9l6S7WzvqALdVWcW5vMUiTnZyj5SPcNwNNANj0koeV1ieqTFQ== +"@lit/reactive-element@^1.0.0", "@lit/reactive-element@^1.1.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.1.1.tgz#523b29e529e881fce47bab764ea1b8058fd45796" + integrity sha512-B2JdRMwCGv+VpIRj3CYVQBx3muPDeE8y+HPgWqzrAHsO5/40BpwDFZeplIV790BaTqDVUDvZOKMSbuFM9zWC0w== "@material/mwc-icon@0.25.3": version "0.25.3" @@ -9505,22 +9505,22 @@ listr@^0.14.3: p-map "^2.0.0" rxjs "^6.3.3" -lit-element@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.0.1.tgz#3c545af17d8a46268bc1dd5623a47486e6ff76f4" - integrity sha512-vs9uybH9ORyK49CFjoNGN85HM9h5bmisU4TQ63phe/+GYlwvY/3SIFYKdjV6xNvzz8v2MnVC+9+QOkPqh+Q3Ew== +lit-element@^3.0.0, lit-element@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.1.1.tgz#562d5ccbc8ba0c01d8ba4a0ac3576263167d2ccb" + integrity sha512-14ClnMAU8EXnzC+M2/KDd3SFmNUn1QUw1+GxWkEMwGV3iaH8ObunMlO5svzvaWlkSV0WlxJCi40NGnDVJ2XZKQ== dependencies: - "@lit/reactive-element" "^1.0.0" - lit-html "^2.0.0" + "@lit/reactive-element" "^1.1.0" + lit-html "^2.1.0" -lit-html@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.0.1.tgz#63241015efa07bc9259b6f96f04abd052d2a1f95" - integrity sha512-KF5znvFdXbxTYM/GjpdOOnMsjgRcFGusTnB54ixnCTya5zUR0XqrDRj29ybuLS+jLXv1jji6Y8+g4W7WP8uL4w== +lit-html@^2.0.0, lit-html@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.1.1.tgz#f4da485798a0d967514d31730d387350fafb79f7" + integrity sha512-E4BImK6lopAYanJpvcGaAG8kQFF1ccIulPu2BRNZI7acFB6i4ujjjsnaPVFT1j/4lD9r8GKih0Y8d7/LH8SeyQ== dependencies: "@types/trusted-types" "^2.0.2" -lit@2.0.2, lit@^2.0.0: +lit@2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/lit/-/lit-2.0.2.tgz#5e6f422924e0732258629fb379556b6d23f7179c" integrity sha512-hKA/1YaSB+P+DvKWuR2q1Xzy/iayhNrJ3aveD0OQ9CKn6wUjsdnF/7LavDOJsKP/K5jzW/kXsuduPgRvTFrFJw== @@ -9529,6 +9529,15 @@ lit@2.0.2, lit@^2.0.0: lit-element "^3.0.0" lit-html "^2.0.0" +lit@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/lit/-/lit-2.1.1.tgz#65f43abca945988f696391f762c645ba51966b0b" + integrity sha512-yqDqf36IhXwOxIQSFqCMgpfvDCRdxLCLZl7m/+tO5C9W/OBHUj17qZpiMBT35v97QMVKcKEi1KZ3hZRyTwBNsQ== + dependencies: + "@lit/reactive-element" "^1.1.0" + lit-element "^3.1.0" + lit-html "^2.1.0" + load-json-file@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" @@ -10250,9 +10259,10 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== -"ninja-keys@https://github.com/chatwoot/ninja-keys.git#b4c3233f676780af90c607866fa85e404c835902": - version "1.1.6" - resolved "https://github.com/chatwoot/ninja-keys.git#b4c3233f676780af90c607866fa85e404c835902" +ninja-keys@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/ninja-keys/-/ninja-keys-1.1.9.tgz#0251c8635f5cb019f5efd667b69b12c293c5cd26" + integrity sha512-XJabKFQIQ2pivNSKA3I/6hm9S7yPhJ2xXDFfWZFOPtDTBOv5xCaqOczrlIaESXPMgjk3xL0izFWUDjSn8ByVvw== dependencies: "@material/mwc-icon" "0.25.3" hotkeys-js "3.8.7" From ab4d63a0061bbff8c28b0e8916ad73af749e0975 Mon Sep 17 00:00:00 2001 From: Anisha-art <96977038+Anisha-art@users.noreply.github.com> Date: Mon, 10 Jan 2022 10:29:16 +0530 Subject: [PATCH 11/22] Error with plurals in Invite members onboarding guide. Issue #3671 (#3718) Updated "email address" to "email addresses" in the Invite your team members guide. --- app/javascript/dashboard/i18n/locale/en/conversation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/dashboard/i18n/locale/en/conversation.json b/app/javascript/dashboard/i18n/locale/en/conversation.json index 5537f9797..80b085fc9 100644 --- a/app/javascript/dashboard/i18n/locale/en/conversation.json +++ b/app/javascript/dashboard/i18n/locale/en/conversation.json @@ -129,7 +129,7 @@ }, "TEAM_MEMBERS": { "TITLE": "Invite your team members", - "DESCRIPTION": "Since you are getting ready to talk to your customer, bring in your teammates to assist you. You can invite your teammates by adding their email address to the agent list.", + "DESCRIPTION": "Since you are getting ready to talk to your customer, bring in your teammates to assist you. You can invite your teammates by adding their email addresses to the agent list.", "NEW_LINK": "Click here to invite a team member" }, "INBOXES": { From 9b6f1df871106c04d16583b438c784c07e454efc Mon Sep 17 00:00:00 2001 From: Prithvi Tharun Date: Mon, 10 Jan 2022 11:56:35 +0530 Subject: [PATCH 12/22] fix: Update copy in canned responses (#3716) Fixes #3669 --- app/javascript/dashboard/i18n/locale/en/cannedMgmt.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/dashboard/i18n/locale/en/cannedMgmt.json b/app/javascript/dashboard/i18n/locale/en/cannedMgmt.json index bcab1dc6a..e86fb87ba 100644 --- a/app/javascript/dashboard/i18n/locale/en/cannedMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/cannedMgmt.json @@ -17,7 +17,7 @@ }, "ADD": { "TITLE": "Add Canned Response", - "DESC": "Canned Responses are saved reply templates which can be used to quickly send out reply to conversation .", + "DESC": "Canned Responses are saved reply templates which can be used to quickly send out reply to conversation.", "CANCEL_BUTTON_TEXT": "Cancel", "FORM": { "SHORT_CODE": { From 9a9462f5cb7b7656d684e9d61b44cdf09ccc03df Mon Sep 17 00:00:00 2001 From: Anisha-art <96977038+Anisha-art@users.noreply.github.com> Date: Mon, 10 Jan 2022 11:57:45 +0530 Subject: [PATCH 13/22] fix: Removed extra space before "." (#3717) Fixes Removed extra space before "." as mentioned in the issue: #3669 --- app/javascript/dashboard/i18n/locale/en/cannedMgmt.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/dashboard/i18n/locale/en/cannedMgmt.json b/app/javascript/dashboard/i18n/locale/en/cannedMgmt.json index e86fb87ba..8c40f8152 100644 --- a/app/javascript/dashboard/i18n/locale/en/cannedMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/cannedMgmt.json @@ -4,7 +4,7 @@ "HEADER_BTN_TXT": "Add Canned Response", "LOADING": "Fetching Canned Responses", "SEARCH_404": "There are no items matching this query", - "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.

", + "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 Chile Date: Mon, 10 Jan 2022 12:41:59 +0530 Subject: [PATCH 14/22] feat: Save automation rules (#3359) --- app/builders/messages/message_builder.rb | 4 +- .../accounts/automation_rules_controller.rb | 21 +++ app/dispatchers/async_dispatcher.rb | 3 +- app/listeners/automation_rule_listener.rb | 29 ++++ .../automation_notification_mailer.rb | 53 ++++++ app/models/account.rb | 1 + app/models/automation_rule.rb | 44 +++++ app/models/concerns/labelable.rb | 5 + app/policies/automation_rule_policy.rb | 9 + .../automation_rules/action_service.rb | 63 +++++++ .../conditions_filter_service.rb | 45 +++++ .../instagram/send_on_instagram_service.rb | 1 - .../automation_rules/create.json.jbuilder | 1 + .../automation_rules/index.json.jbuilder | 5 + .../partials/_automation_rule.json.jbuilder | 7 + .../conversation_creation.liquid | 8 + config/routes.rb | 1 + .../20211110101046_create_automation_rules.rb | 14 ++ db/schema.rb | 12 ++ lib/automation_rules/conditions.json | 160 ++++++++++++++++++ .../automation_rules_controller_spec.rb | 118 +++++++++++++ spec/factories/automation_rules.rb | 19 +++ .../automation_rule_listener_spec.rb | 72 ++++++++ spec/models/automation_rule_spec.rb | 54 ++++++ 24 files changed, 746 insertions(+), 3 deletions(-) create mode 100644 app/controllers/api/v1/accounts/automation_rules_controller.rb create mode 100644 app/listeners/automation_rule_listener.rb create mode 100644 app/mailers/team_notifications/automation_notification_mailer.rb create mode 100644 app/models/automation_rule.rb create mode 100644 app/policies/automation_rule_policy.rb create mode 100644 app/services/automation_rules/action_service.rb create mode 100644 app/services/automation_rules/conditions_filter_service.rb create mode 100644 app/views/api/v1/accounts/automation_rules/create.json.jbuilder create mode 100644 app/views/api/v1/accounts/automation_rules/index.json.jbuilder create mode 100644 app/views/api/v1/accounts/automation_rules/partials/_automation_rule.json.jbuilder create mode 100644 app/views/mailers/team_notifications/automation_notification_mailer/conversation_creation.liquid create mode 100644 db/migrate/20211110101046_create_automation_rules.rb create mode 100644 lib/automation_rules/conditions.json create mode 100644 spec/controllers/api/v1/accounts/automation_rules_controller_spec.rb create mode 100644 spec/factories/automation_rules.rb create mode 100644 spec/listeners/automation_rule_listener_spec.rb create mode 100644 spec/models/automation_rule_spec.rb diff --git a/app/builders/messages/message_builder.rb b/app/builders/messages/message_builder.rb index a30394edc..f84e38ca7 100644 --- a/app/builders/messages/message_builder.rb +++ b/app/builders/messages/message_builder.rb @@ -8,9 +8,11 @@ class Messages::MessageBuilder @conversation = conversation @user = user @message_type = params[:message_type] || 'outgoing' - @items = params.to_unsafe_h&.dig(:content_attributes, :items) @attachments = params[:attachments] + return unless params.instance_of?(ActionController::Parameters) + @in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to) + @items = params.to_unsafe_h&.dig(:content_attributes, :items) end def perform diff --git a/app/controllers/api/v1/accounts/automation_rules_controller.rb b/app/controllers/api/v1/accounts/automation_rules_controller.rb new file mode 100644 index 000000000..0ec35c765 --- /dev/null +++ b/app/controllers/api/v1/accounts/automation_rules_controller.rb @@ -0,0 +1,21 @@ +class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseController + before_action :check_authorization + + def index + @automation_rules = Current.account.automation_rules + end + + def create + @automation_rule = Current.account.automation_rules.create(automation_rules_permit) + end + + private + + def automation_rules_permit + params.permit( + :name, :description, :event_name, :account_id, + conditions: [:attribute_key, :filter_operator, :query_operator, { values: [] }], + actions: [:action_name, { action_params: [:intiated_at] }] + ) + end +end diff --git a/app/dispatchers/async_dispatcher.rb b/app/dispatchers/async_dispatcher.rb index 232dcff77..3f95405f9 100644 --- a/app/dispatchers/async_dispatcher.rb +++ b/app/dispatchers/async_dispatcher.rb @@ -16,7 +16,8 @@ class AsyncDispatcher < BaseDispatcher HookListener.instance, InstallationWebhookListener.instance, NotificationListener.instance, - WebhookListener.instance + WebhookListener.instance, + AutomationRuleListener.instance ] end end diff --git a/app/listeners/automation_rule_listener.rb b/app/listeners/automation_rule_listener.rb new file mode 100644 index 000000000..9562e4ffe --- /dev/null +++ b/app/listeners/automation_rule_listener.rb @@ -0,0 +1,29 @@ +class AutomationRuleListener < BaseListener + def conversation_status_changed(event_obj) + conversation = event_obj.data[:conversation] + return unless rule_present?('conversation_status_changed', conversation) + + @rules.each do |rule| + conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, conversation).perform + AutomationRules::ActionService.new(rule, conversation).perform if conditions_match.present? + end + end + + def conversation_created(event_obj) + conversation = event_obj.data[:conversation] + return unless rule_present?('conversation_created', conversation) + + @rules.each do |rule| + conditions_match = AutomationRule::ConditionsFilterService.new(rule, conversation).perform + AutomationRule::ActionService.new(rule, conversation).perform if conditions_match.present? + end + end + + def rule_present?(event_name, conversation) + @rules = AutomationRule.where( + event_name: event_name, + account_id: conversation.account_id + ) + @rules.any? + end +end diff --git a/app/mailers/team_notifications/automation_notification_mailer.rb b/app/mailers/team_notifications/automation_notification_mailer.rb new file mode 100644 index 000000000..a6d219c54 --- /dev/null +++ b/app/mailers/team_notifications/automation_notification_mailer.rb @@ -0,0 +1,53 @@ +class TeamNotifications::AutomationNotificationMailer < ApplicationMailer + def conversation_creation(conversation, team, message) + return unless smtp_config_set_or_development? + + @agents = team.team_members + @conversation = conversation + @message = message + @action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id) + + send_an_email_to_team + end + + def conversation_updated(conversation, team) + return unless smtp_config_set_or_development? + + @agents = team.team_members + @conversation = conversation + @message = message + @action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id) + + send_an_email_to_team + end + + def message_created(message, agent) + return unless smtp_config_set_or_development? + + @agent = agent + @conversation = message.conversation + @message = message + subject = "#{@agent.available_name}, You have been mentioned in conversation [ID - #{@conversation.display_id}]" + @action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id) + send_mail_with_liquid(to: @agent.email, subject: subject) + end + + private + + def send_an_email_to_team + @agents.each do |agent| + subject = "#{@agent.available_name}, A new conversation [ID - #{@conversation.display_id}] has been created in #{@conversation.inbox&.name}." + @action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id) + send_mail_with_liquid(to: agent.email, subject: subject) + end + end + + def liquid_droppables + super.merge({ + user: @agent, + conversation: @conversation, + inbox: @conversation.inbox, + message: @message + }) + end +end diff --git a/app/models/account.rb b/app/models/account.rb index 657517c8d..c905265a9 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -69,6 +69,7 @@ class Account < ApplicationRecord has_many :webhooks, dependent: :destroy_async has_many :whatsapp_channels, dependent: :destroy_async, class_name: '::Channel::Whatsapp' has_many :working_hours, dependent: :destroy_async + has_many :automation_rules, dependent: :destroy has_flags ACCOUNT_SETTINGS_FLAGS.merge(column: 'settings_flags').merge(DEFAULT_QUERY_SETTING) diff --git a/app/models/automation_rule.rb b/app/models/automation_rule.rb new file mode 100644 index 000000000..84ae6b51c --- /dev/null +++ b/app/models/automation_rule.rb @@ -0,0 +1,44 @@ +# == Schema Information +# +# Table name: automation_rules +# +# id :bigint not null, primary key +# actions :jsonb not null +# conditions :jsonb not null +# description :text +# event_name :string not null +# name :string not null +# created_at :datetime not null +# updated_at :datetime not null +# account_id :bigint not null +# +# Indexes +# +# index_automation_rules_on_account_id (account_id) +# +class AutomationRule < ApplicationRecord + belongs_to :account + + validates :account, presence: true + validate :json_conditions_format + validate :json_actions_format + + CONDITIONS_ATTRS = %w[country_code status browser_language assignee_id team_id referer].freeze + ACTIONS_ATTRS = %w[send_message add_label send_email_to_team assign_team assign_best_agents].freeze + + private + + def json_conditions_format + return if conditions.nil? + + attributes = conditions.map { |obj, _| obj['attribute_key'] } + (attributes - CONDITIONS_ATTRS).blank? + end + + def json_actions_format + return if actions.nil? + + attributes = actions.map { |obj, _| obj['attribute_key'] } + (attributes - ACTIONS_ATTRS).blank? + end +end diff --git a/app/models/concerns/labelable.rb b/app/models/concerns/labelable.rb index 4e654fdd4..5a71fe84f 100644 --- a/app/models/concerns/labelable.rb +++ b/app/models/concerns/labelable.rb @@ -8,4 +8,9 @@ module Labelable def update_labels(labels = nil) update!(label_list: labels) end + + def add_labels(new_labels = nil) + new_labels << labels + update!(label_list: new_labels) + end end diff --git a/app/policies/automation_rule_policy.rb b/app/policies/automation_rule_policy.rb new file mode 100644 index 000000000..bd644f075 --- /dev/null +++ b/app/policies/automation_rule_policy.rb @@ -0,0 +1,9 @@ +class AutomationRulePolicy < ApplicationPolicy + def index? + @account_user.administrator? + end + + def create? + @account_user.administrator? + end +end diff --git a/app/services/automation_rules/action_service.rb b/app/services/automation_rules/action_service.rb new file mode 100644 index 000000000..28327959a --- /dev/null +++ b/app/services/automation_rules/action_service.rb @@ -0,0 +1,63 @@ +class AutomationRules::ActionService + def initialize(rule, conversation) + @rule = rule + @conversation = conversation + @account = @conversation.account + end + + def perform + @rule.actions.each do |action, _current_index| + action = action.with_indifferent_access + send(action[:action_name], action[:action_params]) + end + end + + private + + def send_message(message) + # params = { content: message, private: false } + # mb = Messages::MessageBuilder.new(@administrator, @conversation, params) + # mb.perform + end + + def assign_team(team_ids = []) + return unless team_belongs_to_account?(team_ids) + + @account.teams.find_by(id: team_ids) + @conversation.update!(team_id: team_ids[0]) + end + + def assign_best_agents(agent_ids = []) + return unless agent_belongs_to_account?(agent_ids) + + @agent = @account.users.find_by(id: agent_ids) + @conversation.update_assignee(@agent) + end + + def add_label(labels = []) + @conversation.add_labels(labels) + end + + def send_email_to_team(params) + team = Team.find(params[:team_ids][0]) + + case @rule.event_name + when 'conversation_created', 'conversation_status_changed' + TeamNotifications::AutomationNotificationMailer.conversation_creation(@conversation, team, params[:message]) + when 'conversation_updated' + TeamNotifications::AutomationNotificationMailer.conversation_updated(@conversation, team, params[:message]) + end + end + + def administrator + @administrator ||= @account.administrators.first + end + + def agent_belongs_to_account?(agent_ids) + @account.agents.pluck(:id).include?(agent_ids[0]) + end + + def team_belongs_to_account?(team_ids) + @account.team_ids.include?(team_ids[0]) + end +end diff --git a/app/services/automation_rules/conditions_filter_service.rb b/app/services/automation_rules/conditions_filter_service.rb new file mode 100644 index 000000000..bce3b581d --- /dev/null +++ b/app/services/automation_rules/conditions_filter_service.rb @@ -0,0 +1,45 @@ +require 'json' + +class AutomationRules::ConditionsFilterService < FilterService + def initialize(rule, conversation) + super([], nil) + @rule = rule + @conversation = conversation + file = File.read('./lib/filters/filter_keys.json') + @filters = JSON.parse(file) + end + + def perform + conversation_filters = @filters['conversations'] + + @rule.conditions.each_with_index do |query_hash, current_index| + current_filter = conversation_filters[query_hash['attribute_key']] + @query_string += conversation_query_string(current_filter, query_hash.with_indifferent_access, current_index) + end + + records = base_relation.where(@query_string, @filter_values.with_indifferent_access) + records.any? + end + + def conversation_query_string(current_filter, query_hash, current_index) + attribute_key = query_hash['attribute_key'] + query_operator = query_hash['query_operator'] + + filter_operator_value = filter_operation(query_hash, current_index) + + case current_filter['attribute_type'] + when 'additional_attributes' + " conversations.additional_attributes ->> '#{attribute_key}' #{filter_operator_value} #{query_operator} " + when 'standard' + if attribute_key == 'labels' + " tags.id #{filter_operator_value} #{query_operator} " + else + " conversations.#{attribute_key} #{filter_operator_value} #{query_operator} " + end + end + end + + def base_relation + Conversation.where(id: @conversation) + end +end diff --git a/app/services/instagram/send_on_instagram_service.rb b/app/services/instagram/send_on_instagram_service.rb index 0a8e9d4ec..8dfaf43ec 100644 --- a/app/services/instagram/send_on_instagram_service.rb +++ b/app/services/instagram/send_on_instagram_service.rb @@ -51,7 +51,6 @@ class Instagram::SendOnInstagramService < Base::SendOnChannelService def send_to_facebook_page(message_content) access_token = channel.page_access_token app_secret_proof = calculate_app_secret_proof(GlobalConfigService.load('FB_APP_SECRET', ''), access_token) - query = { access_token: access_token } query[:appsecret_proof] = app_secret_proof if app_secret_proof diff --git a/app/views/api/v1/accounts/automation_rules/create.json.jbuilder b/app/views/api/v1/accounts/automation_rules/create.json.jbuilder new file mode 100644 index 000000000..f2f893279 --- /dev/null +++ b/app/views/api/v1/accounts/automation_rules/create.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'api/v1/accounts/automation_rules/partials/automation_rule.json.jbuilder', automation_rule: @automation_rule diff --git a/app/views/api/v1/accounts/automation_rules/index.json.jbuilder b/app/views/api/v1/accounts/automation_rules/index.json.jbuilder new file mode 100644 index 000000000..266ab14b8 --- /dev/null +++ b/app/views/api/v1/accounts/automation_rules/index.json.jbuilder @@ -0,0 +1,5 @@ +json.data do + json.array! @automation_rules do |automation_rule| + json.partial! 'api/v1/accounts/automation_rules/partials/automation_rule.json.jbuilder', automation_rule: automation_rule + end +end diff --git a/app/views/api/v1/accounts/automation_rules/partials/_automation_rule.json.jbuilder b/app/views/api/v1/accounts/automation_rules/partials/_automation_rule.json.jbuilder new file mode 100644 index 000000000..9799a4339 --- /dev/null +++ b/app/views/api/v1/accounts/automation_rules/partials/_automation_rule.json.jbuilder @@ -0,0 +1,7 @@ +json.id automation_rule.id +json.account_id automation_rule.account_id +json.name automation_rule.name +json.description automation_rule.description +json.event_name automation_rule.event_name +json.conditions automation_rule.conditions +json.actions automation_rule.actions diff --git a/app/views/mailers/team_notifications/automation_notification_mailer/conversation_creation.liquid b/app/views/mailers/team_notifications/automation_notification_mailer/conversation_creation.liquid new file mode 100644 index 000000000..e324dd9e6 --- /dev/null +++ b/app/views/mailers/team_notifications/automation_notification_mailer/conversation_creation.liquid @@ -0,0 +1,8 @@ +

Hi {{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" Date: Tue, 11 Jan 2022 00:32:03 -0800 Subject: [PATCH 15/22] feat: Toggle to disallow users to send messages after a conversation is resolved (#3605) --- .../api/v1/accounts/inboxes_controller.rb | 2 +- .../dashboard/i18n/locale/en/inboxMgmt.json | 8 ++++- .../dashboard/settings/inbox/Settings.vue | 22 +++++++++++++ .../widget/components/ChatFooter.vue | 4 +-- app/models/inbox.rb | 33 ++++++++++--------- app/views/api/v1/models/_inbox.json.jbuilder | 1 + app/views/widgets/show.html.erb | 3 +- ..._allow_messages_after_resolved_to_inbox.rb | 16 +++++++++ db/schema.rb | 3 +- .../v1/accounts/inboxes_controller_spec.rb | 21 ++++++++++++ 10 files changed, 91 insertions(+), 22 deletions(-) create mode 100644 db/migrate/20211216110209_add_allow_messages_after_resolved_to_inbox.rb diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 1720ee692..9f2cfba8c 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -118,7 +118,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController def permitted_params(channel_attributes = []) params.permit( :name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled, - :enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, + :enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved, channel: [:type, *channel_attributes] ) end diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index b64362027..fb1d5c35f 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -306,6 +306,10 @@ "ENABLED": "Enabled", "DISABLED": "Disabled" }, + "ALLOW_MESSAGES_AFTER_RESOLVED": { + "ENABLED": "Enabled", + "DISABLED": "Disabled" + }, "ENABLE_HMAC": { "LABEL": "Enable" } @@ -362,7 +366,9 @@ "INBOX_IDENTIFIER": "Inbox Identifier", "INBOX_IDENTIFIER_SUB_TEXT": "Use the `inbox_identifier` token shown here to authentication your API clients.", "FORWARD_EMAIL_TITLE": "Forward to Email", - "FORWARD_EMAIL_SUB_TEXT": "Start forwarding your emails to the following email address." + "FORWARD_EMAIL_SUB_TEXT": "Start forwarding your emails to the following email address.", + "ALLOW_MESSAGES_AFTER_RESOLVED": "Allow messages after conversation resolved", + "ALLOW_MESSAGES_AFTER_RESOLVED_SUB_TEXT": "Allow the end-users to send messages even after the conversation is resolved." }, "FACEBOOK_REAUTHORIZE": { "TITLE": "Reauthorize", diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue index cb61c2f97..79acdfef2 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue @@ -215,6 +215,25 @@

+ + @@ -420,6 +439,7 @@ export default { emailCollectEnabled: false, isAgentListUpdating: false, csatSurveyEnabled: false, + allowMessagesAfterResolved: true, selectedInboxName: '', channelWebsiteUrl: '', webhookUrl: '', @@ -583,6 +603,7 @@ export default { this.autoAssignment = this.inbox.enable_auto_assignment; this.emailCollectEnabled = this.inbox.enable_email_collect; this.csatSurveyEnabled = this.inbox.csat_survey_enabled; + this.allowMessagesAfterResolved = this.inbox.allow_messages_after_resolved; this.channelWebsiteUrl = this.inbox.website_url; this.channelWelcomeTitle = this.inbox.welcome_title; this.channelWelcomeTagline = this.inbox.welcome_tagline; @@ -625,6 +646,7 @@ export default { enable_auto_assignment: this.autoAssignment, enable_email_collect: this.emailCollectEnabled, csat_survey_enabled: this.csatSurveyEnabled, + allow_messages_after_resolved: this.allowMessagesAfterResolved, greeting_enabled: this.greetingEnabled, greeting_message: this.greetingMessage || '', channel: { diff --git a/app/javascript/widget/components/ChatFooter.vue b/app/javascript/widget/components/ChatFooter.vue index 58538d0c6..7561f1930 100755 --- a/app/javascript/widget/components/ChatFooter.vue +++ b/app/javascript/widget/components/ChatFooter.vue @@ -58,9 +58,9 @@ export default { return getContrastingTextColor(this.widgetColor); }, hideReplyBox() { - const { csatSurveyEnabled } = window.chatwootWebChannel; + const { allowMessagesAfterResolved } = window.chatwootWebChannel; const { status } = this.conversationAttributes; - return csatSurveyEnabled && status === 'resolved'; + return !allowMessagesAfterResolved && status === 'resolved'; }, showEmailTranscriptButton() { return this.currentUser && this.currentUser.email; diff --git a/app/models/inbox.rb b/app/models/inbox.rb index f16a5ca92..774c66f9d 100644 --- a/app/models/inbox.rb +++ b/app/models/inbox.rb @@ -4,22 +4,23 @@ # # Table name: inboxes # -# id :integer not null, primary key -# channel_type :string -# csat_survey_enabled :boolean default(FALSE) -# email_address :string -# enable_auto_assignment :boolean default(TRUE) -# enable_email_collect :boolean default(TRUE) -# greeting_enabled :boolean default(FALSE) -# greeting_message :string -# name :string not null -# out_of_office_message :string -# timezone :string default("UTC") -# working_hours_enabled :boolean default(FALSE) -# created_at :datetime not null -# updated_at :datetime not null -# account_id :integer not null -# channel_id :integer not null +# id :integer not null, primary key +# allow_messages_after_resolved :boolean default(TRUE) +# channel_type :string +# csat_survey_enabled :boolean default(FALSE) +# email_address :string +# enable_auto_assignment :boolean default(TRUE) +# enable_email_collect :boolean default(TRUE) +# greeting_enabled :boolean default(FALSE) +# greeting_message :string +# name :string not null +# out_of_office_message :string +# timezone :string default("UTC") +# working_hours_enabled :boolean default(FALSE) +# created_at :datetime not null +# updated_at :datetime not null +# account_id :integer not null +# channel_id :integer not null # # Indexes # diff --git a/app/views/api/v1/models/_inbox.json.jbuilder b/app/views/api/v1/models/_inbox.json.jbuilder index f76b8566d..eca35c466 100644 --- a/app/views/api/v1/models/_inbox.json.jbuilder +++ b/app/views/api/v1/models/_inbox.json.jbuilder @@ -13,6 +13,7 @@ json.out_of_office_message resource.out_of_office_message json.working_hours resource.weekly_schedule json.timezone resource.timezone json.callback_webhook_url resource.callback_webhook_url +json.allow_messages_after_resolved resource.allow_messages_after_resolved json.tweets_enabled resource.channel.try(:tweets_enabled) if resource.twitter? diff --git a/app/views/widgets/show.html.erb b/app/views/widgets/show.html.erb index 0cd60c453..02361ce7b 100644 --- a/app/views/widgets/show.html.erb +++ b/app/views/widgets/show.html.erb @@ -23,7 +23,8 @@ csatSurveyEnabled: <%= @web_widget.inbox.csat_survey_enabled %>, workingHours: <%= @web_widget.inbox.working_hours.to_json.html_safe %>, outOfOfficeMessage: <%= @web_widget.inbox.out_of_office_message.to_json.html_safe %>, - utcOffset: '<%= ActiveSupport::TimeZone[@web_widget.inbox.timezone].now.formatted_offset %>' + utcOffset: '<%= ActiveSupport::TimeZone[@web_widget.inbox.timezone].now.formatted_offset %>', + allowMessagesAfterResolved: <%= @web_widget.inbox.allow_messages_after_resolved %> } window.chatwootWidgetDefaults = { useInboxAvatarForBot: <%= ActiveModel::Type::Boolean.new.cast(ENV.fetch('USE_INBOX_AVATAR_FOR_BOT', false)) %>, diff --git a/db/migrate/20211216110209_add_allow_messages_after_resolved_to_inbox.rb b/db/migrate/20211216110209_add_allow_messages_after_resolved_to_inbox.rb new file mode 100644 index 000000000..4c44ac2bb --- /dev/null +++ b/db/migrate/20211216110209_add_allow_messages_after_resolved_to_inbox.rb @@ -0,0 +1,16 @@ +class AddAllowMessagesAfterResolvedToInbox < ActiveRecord::Migration[6.1] + def change + add_column :inboxes, :allow_messages_after_resolved, :boolean, default: true + + update_csat_enabled_inboxes + end + + def update_csat_enabled_inboxes + ::Inbox.where(channel_type: 'Channel::WebWidget', csat_survey_enabled: true).find_in_batches do |inboxes_batch| + inboxes_batch.each do |inbox| + inbox.allow_messages_after_resolved = false + inbox.save! + end + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 14b64b78a..ad5bc66b0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -461,6 +461,7 @@ ActiveRecord::Schema.define(version: 2021_12_21_125545) do t.string "timezone", default: "UTC" t.boolean "enable_email_collect", default: true t.boolean "csat_survey_enabled", default: false + t.boolean 'allow_messages_after_resolved', default: true t.index ["account_id"], name: "index_inboxes_on_account_id" end @@ -829,4 +830,4 @@ ActiveRecord::Schema.define(version: 2021_12_21_125545) do "NEW.display_id := nextval('camp_dpid_seq_' || NEW.account_id);" end -end +end \ No newline at end of file diff --git a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb index ea28dcbd5..1c7082abc 100644 --- a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb @@ -308,6 +308,17 @@ RSpec.describe 'Inboxes API', type: :request do expect(response.body).to include('Line Inbox') expect(response.body).to include('callback_webhook_url') end + + it 'creates the webwidget inbox that allow messages after conversation is resolved' do + post "/api/v1/accounts/#{account.id}/inboxes", + headers: admin.create_new_auth_token, + params: valid_params, + as: :json + + expect(response).to have_http_status(:success) + json_response = JSON.parse(response.body) + expect(json_response['allow_messages_after_resolved']).to be true + end end end @@ -467,6 +478,16 @@ RSpec.describe 'Inboxes API', type: :request do inbox.reload expect(inbox.reload.weekly_schedule.find { |schedule| schedule['day_of_week'] == 0 }['open_hour']).to eq 9 end + + it 'updates the webwidget inbox to disallow the messages after conversation is resolved' do + patch "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}", + headers: admin.create_new_auth_token, + params: valid_params.merge({ allow_messages_after_resolved: false }), + as: :json + + expect(response).to have_http_status(:success) + expect(inbox.reload.allow_messages_after_resolved).to be_falsey + end end end From 8a26d78623d95b675f7b338b1e3b271af88267e5 Mon Sep 17 00:00:00 2001 From: Axel Tietje Date: Tue, 11 Jan 2022 20:06:57 +0100 Subject: [PATCH 16/22] chore: fix german translations (#3724) --- config/locales/de.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/locales/de.yml b/config/locales/de.yml index 7df37a662..5576de060 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -53,8 +53,8 @@ de: deleted: Diese Nachricht wurde gelöscht activity: status: - resolved: "Das Gespräch wurde von gelöst gelöst %{user_name}" - open: "Das Gespräch wurde von wieder eröffnet %{user_name}" + resolved: "Das Gespräch wurde von %{user_name} gelöst" + open: "Das Gespräch wurde von %{user_name} wieder eröffnet" pending: "Das Gespräch wurde von %{user_name} als ausstehend markiert" snoozed: "Das Gespräch wurde von %{user_name} zur Erinnerung markiert" auto_resolved: "Das Gespräch wurde vom System aufgrund von %{duration} Tagen Inaktivität gelöst" From de37975be45cbe643f88cb3c8b7b7b247b07476d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Jan 2022 13:25:44 -0800 Subject: [PATCH 17/22] chore(deps): bump google-protobuf from 3.18.1 to 3.19.2 (#3714) Bumps [google-protobuf](https://github.com/protocolbuffers/protobuf) from 3.18.1 to 3.19.2. - [Release notes](https://github.com/protocolbuffers/protobuf/releases) - [Changelog](https://github.com/protocolbuffers/protobuf/blob/master/generate_changelog.py) - [Commits](https://github.com/protocolbuffers/protobuf/compare/v3.18.1...v3.19.2) --- updated-dependencies: - dependency-name: google-protobuf dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index b61ef372c..079dd0ee6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -255,9 +255,9 @@ GEM google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - google-protobuf (3.18.1) - google-protobuf (3.18.1-universal-darwin) - google-protobuf (3.18.1-x86_64-linux) + google-protobuf (3.19.2) + google-protobuf (3.19.2-x86_64-darwin) + google-protobuf (3.19.2-x86_64-linux) googleapis-common-protos (1.3.12) google-protobuf (~> 3.14) googleapis-common-protos-types (~> 1.2) From ebb04487a8569fd96ce0dbe1a46cca043e1d2ba0 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Wed, 12 Jan 2022 04:44:55 +0530 Subject: [PATCH 18/22] chore: Prevent null in Contact JSONB attributes (#3730) --- .rubocop.yml | 1 + app/models/contact.rb | 15 +++++++-- ...te_nil_contact_attributes_to_empty_hash.rb | 8 +++++ db/schema.rb | 6 ++-- spec/models/contact_spec.rb | 32 +++++++++++++++++++ 5 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 db/migrate/20220111200105_update_nil_contact_attributes_to_empty_hash.rb diff --git a/.rubocop.yml b/.rubocop.yml index e9964b1c0..34d5b0bda 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -11,6 +11,7 @@ Metrics/ClassLength: Max: 125 Exclude: - 'app/models/conversation.rb' + - 'app/models/contact.rb' - 'app/mailers/conversation_reply_mailer.rb' - 'app/models/message.rb' - 'app/builders/messages/facebook/message_builder.rb' diff --git a/app/models/contact.rb b/app/models/contact.rb index 7b257bab4..ebddb6828 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -45,7 +45,7 @@ class Contact < ApplicationRecord has_many :messages, as: :sender, dependent: :destroy_async has_many :notes, dependent: :destroy_async - before_validation :prepare_email_attribute + before_validation :prepare_contact_attributes after_create_commit :dispatch_create_event, :ip_lookup after_update_commit :dispatch_update_event after_destroy_commit :dispatch_destroy_event @@ -146,10 +146,19 @@ class Contact < ApplicationRecord ContactIpLookupJob.perform_later(self) end + def prepare_contact_attributes + prepare_email_attribute + prepare_jsonb_attributes + end + def prepare_email_attribute # So that the db unique constraint won't throw error when email is '' - self.email = nil if email.blank? - email.downcase! if email.present? + self.email = email.present? ? email.downcase : nil + end + + def prepare_jsonb_attributes + self.additional_attributes = {} if additional_attributes.blank? + self.custom_attributes = {} if custom_attributes.blank? end def dispatch_create_event diff --git a/db/migrate/20220111200105_update_nil_contact_attributes_to_empty_hash.rb b/db/migrate/20220111200105_update_nil_contact_attributes_to_empty_hash.rb new file mode 100644 index 000000000..31881fe1d --- /dev/null +++ b/db/migrate/20220111200105_update_nil_contact_attributes_to_empty_hash.rb @@ -0,0 +1,8 @@ +class UpdateNilContactAttributesToEmptyHash < ActiveRecord::Migration[6.1] + def change + # rubocop:disable Rails/SkipsModelValidations + Contact.where(custom_attributes: nil).update_all(custom_attributes: {}) + Contact.where(additional_attributes: nil).update_all(additional_attributes: {}) + # rubocop:enable Rails/SkipsModelValidations + end +end diff --git a/db/schema.rb b/db/schema.rb index ad5bc66b0..34bcdce36 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.define(version: 2021_12_21_125545) do +ActiveRecord::Schema.define(version: 2022_01_11_200105) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" @@ -461,7 +461,7 @@ ActiveRecord::Schema.define(version: 2021_12_21_125545) do t.string "timezone", default: "UTC" t.boolean "enable_email_collect", default: true t.boolean "csat_survey_enabled", default: false - t.boolean 'allow_messages_after_resolved', default: true + t.boolean "allow_messages_after_resolved", default: true t.index ["account_id"], name: "index_inboxes_on_account_id" end @@ -830,4 +830,4 @@ ActiveRecord::Schema.define(version: 2021_12_21_125545) do "NEW.display_id := nextval('camp_dpid_seq_' || NEW.account_id);" end -end \ No newline at end of file +end diff --git a/spec/models/contact_spec.rb b/spec/models/contact_spec.rb index 4306e9e44..f8b701a3a 100644 --- a/spec/models/contact_spec.rb +++ b/spec/models/contact_spec.rb @@ -11,4 +11,36 @@ RSpec.describe Contact do it { is_expected.to belong_to(:account) } it { is_expected.to have_many(:conversations).dependent(:destroy_async) } end + + context 'pepare contact attributes before validation' do + it 'sets email to lowercase' do + contact = create(:contact, email: 'Test@test.com') + expect(contact.email).to eq('test@test.com') + end + + it 'sets email to nil when empty string' do + contact = create(:contact, email: '') + expect(contact.email).to be_nil + end + + it 'sets custom_attributes to {} when nil' do + contact = create(:contact, custom_attributes: nil) + expect(contact.custom_attributes).to eq({}) + end + + it 'sets custom_attributes to {} when empty string' do + contact = create(:contact, custom_attributes: '') + expect(contact.custom_attributes).to eq({}) + end + + it 'sets additional_attributes to {} when nil' do + contact = create(:contact, additional_attributes: nil) + expect(contact.additional_attributes).to eq({}) + end + + it 'sets additional_attributes to {} when empty string' do + contact = create(:contact, additional_attributes: '') + expect(contact.additional_attributes).to eq({}) + end + end end From bec1266242db137eb8715482259df9e76fdfd968 Mon Sep 17 00:00:00 2001 From: Prithvi Tharun Date: Wed, 12 Jan 2022 04:50:52 +0530 Subject: [PATCH 19/22] fix: Update shortcode -> short code (#3708) Closes #3672 --- app/javascript/dashboard/i18n/locale/en/cannedMgmt.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/dashboard/i18n/locale/en/cannedMgmt.json b/app/javascript/dashboard/i18n/locale/en/cannedMgmt.json index 8c40f8152..9c14f5a52 100644 --- a/app/javascript/dashboard/i18n/locale/en/cannedMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/cannedMgmt.json @@ -22,7 +22,7 @@ "FORM": { "SHORT_CODE": { "LABEL": "Short Code", - "PLACEHOLDER": "Please enter a shortcode", + "PLACEHOLDER": "Please enter a short code", "ERROR": "Short Code is required" }, "CONTENT": { From f44be0b1e65ed5d7b93052ce2b6c6927a277224c Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Wed, 12 Jan 2022 08:30:00 +0530 Subject: [PATCH 20/22] fix: SuperAdmin Improvements (#3733) - Fix broken access tokens pages - Fix broken avatar images in the super admin users tab - Fix broken pagination links in super admin - Add agent bot tabs Fixes: #2021 --- app/dashboards/agent_bot_dashboard.rb | 5 +- app/fields/avatar_field.rb | 2 +- app/models/platform_app_permissible.rb | 4 +- app/views/fields/avatar_field/_index.html.erb | 2 +- app/views/fields/avatar_field/_show.html.erb | 2 +- .../application/_navigation.html.erb | 5 +- .../super_admin/application/index.html.erb | 2 +- app/views/super_admin/users/index.html.erb | 66 ------------------ ..._platform_app_permissible_access_tokens.rb | 5 ++ db/schema.rb | 2 +- public/admin/avatar.png | Bin 8512 -> 0 bytes public/admin/avatar_square.png | Bin 4514 -> 0 bytes 12 files changed, 18 insertions(+), 77 deletions(-) delete mode 100644 app/views/super_admin/users/index.html.erb create mode 100644 db/migrate/20220111223630_remove_platform_app_permissible_access_tokens.rb delete mode 100644 public/admin/avatar.png delete mode 100644 public/admin/avatar_square.png diff --git a/app/dashboards/agent_bot_dashboard.rb b/app/dashboards/agent_bot_dashboard.rb index 87605135d..e97a64de8 100644 --- a/app/dashboards/agent_bot_dashboard.rb +++ b/app/dashboards/agent_bot_dashboard.rb @@ -12,6 +12,7 @@ class AgentBotDashboard < Administrate::BaseDashboard avatar_url: AvatarField, id: Field::Number, name: Field::String, + account: Field::BelongsTo.with_options(searchable: true, searchable_field: 'name', order: 'id DESC'), description: Field::String, outgoing_url: Field::String, created_at: Field::DateTime, @@ -26,6 +27,7 @@ class AgentBotDashboard < Administrate::BaseDashboard COLLECTION_ATTRIBUTES = %i[ id avatar_url + account name outgoing_url ].freeze @@ -34,7 +36,7 @@ class AgentBotDashboard < Administrate::BaseDashboard # an array of attributes that will be displayed on the model's show page. SHOW_PAGE_ATTRIBUTES = %i[ id - avatar_url + account name description outgoing_url @@ -45,6 +47,7 @@ class AgentBotDashboard < Administrate::BaseDashboard # on the model's form (`new` and `edit`) pages. FORM_ATTRIBUTES = %i[ name + account description outgoing_url ].freeze diff --git a/app/fields/avatar_field.rb b/app/fields/avatar_field.rb index 50633ccd2..a9674eb94 100644 --- a/app/fields/avatar_field.rb +++ b/app/fields/avatar_field.rb @@ -2,6 +2,6 @@ require 'administrate/field/base' class AvatarField < Administrate::Field::Base def avatar_url - data.presence || '/admin/avatar.png' + data.presence&.gsub('?d=404', '?d=mp') end end diff --git a/app/models/platform_app_permissible.rb b/app/models/platform_app_permissible.rb index 14b804e84..94852dbac 100644 --- a/app/models/platform_app_permissible.rb +++ b/app/models/platform_app_permissible.rb @@ -16,11 +16,9 @@ # unique_permissibles_index (platform_app_id,permissible_id,permissible_type) UNIQUE # class PlatformAppPermissible < ApplicationRecord - include AccessTokenable - validates :platform_app, presence: true validates :platform_app_id, uniqueness: { scope: [:permissible_id, :permissible_type] } belongs_to :platform_app - belongs_to :permissible, polymorphic: true, dependent: :destroy_async + belongs_to :permissible, polymorphic: true end diff --git a/app/views/fields/avatar_field/_index.html.erb b/app/views/fields/avatar_field/_index.html.erb index 61f2eff47..408739ece 100644 --- a/app/views/fields/avatar_field/_index.html.erb +++ b/app/views/fields/avatar_field/_index.html.erb @@ -1 +1 @@ -<%= image_tag field.avatar_url %> +<%= image_tag field.avatar_url if field.avatar_url.present? %> diff --git a/app/views/fields/avatar_field/_show.html.erb b/app/views/fields/avatar_field/_show.html.erb index 61f2eff47..408739ece 100644 --- a/app/views/fields/avatar_field/_show.html.erb +++ b/app/views/fields/avatar_field/_show.html.erb @@ -1 +1 @@ -<%= image_tag field.avatar_url %> +<%= image_tag field.avatar_url if field.avatar_url.present? %> diff --git a/app/views/super_admin/application/_navigation.html.erb b/app/views/super_admin/application/_navigation.html.erb index d55927c86..fb4fd9018 100644 --- a/app/views/super_admin/application/_navigation.html.erb +++ b/app/views/super_admin/application/_navigation.html.erb @@ -17,7 +17,8 @@ as defined by the routes in the `admin/` namespace super_admins: 'ion ion-unlocked', access_tokens: 'ion-key', platform_apps: 'ion ion-social-buffer', - installation_configs: 'ion ion-settings' + installation_configs: 'ion ion-settings', + agent_bots: 'ion ion-social-android', } %> @@ -39,7 +40,7 @@ as defined by the routes in the `admin/` namespace <% Administrate::Namespace.new(namespace).resources.each do |resource| %> - <% next if ["account_users", "agent_bots", "dashboard", "devise/sessions", "app_configs" ].include? resource.resource %> + <% next if ["account_users", "dashboard", "devise/sessions", "app_configs" ].include? resource.resource %>