From c9f821cc5b6768456192d12520acbc839ddb4b7a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 31 Mar 2022 16:02:53 +0530 Subject: [PATCH 001/177] chore(deps): bump puma from 5.6.2 to 5.6.4 (#4348) Bumps [puma](https://github.com/puma/puma) from 5.6.2 to 5.6.4. - [Release notes](https://github.com/puma/puma/releases) - [Changelog](https://github.com/puma/puma/blob/master/History.md) - [Commits](https://github.com/puma/puma/compare/v5.6.2...v5.6.4) --- updated-dependencies: - dependency-name: puma dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index eb6022ac7..6c314817d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -403,7 +403,7 @@ GEM pry-rails (0.3.9) pry (>= 0.10.4) public_suffix (4.0.6) - puma (5.6.2) + puma (5.6.4) nio4r (~> 2.0) pundit (2.2.0) activesupport (>= 3.0.0) From 0477123f92234c7e1e060ce5e190f90eac0af9a8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 31 Mar 2022 16:33:14 +0530 Subject: [PATCH 002/177] chore(deps): bump minimist from 1.2.5 to 1.2.6 (#4351) Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6. - [Release notes](https://github.com/substack/minimist/releases) - [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6) --- updated-dependencies: - dependency-name: minimist dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 931d979a6..9acb20d6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10090,9 +10090,9 @@ minimatch@3.0.4, minimatch@^3.0.2, minimatch@^3.0.4: brace-expansion "^1.1.7" minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== minipass-collect@^1.0.2: version "1.0.2" From eff3a5031641b4b655806109315b1ea52f342588 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Thu, 31 Mar 2022 17:35:39 +0530 Subject: [PATCH 003/177] fix: Disable showing read messages in unread view (#4324) --- app/javascript/widget/App.vue | 9 +++++- .../store/modules/conversation/actions.js | 8 ++++-- .../specs/conversation/actions.spec.js | 28 +++++++++++-------- .../v1/widget/messages/index.json.jbuilder | 25 ++++++++++------- .../api/v1/widget/messages_controller_spec.rb | 3 +- 5 files changed, 47 insertions(+), 26 deletions(-) diff --git a/app/javascript/widget/App.vue b/app/javascript/widget/App.vue index 8b34c51bf..9d6600264 100755 --- a/app/javascript/widget/App.vue +++ b/app/javascript/widget/App.vue @@ -138,7 +138,13 @@ export default { } }, registerUnreadEvents() { - bus.$on(ON_AGENT_MESSAGE_RECEIVED, this.setUnreadView); + bus.$on(ON_AGENT_MESSAGE_RECEIVED, () => { + const { name: routeName } = this.$route; + if (this.isWidgetOpen && routeName === 'messages') { + this.$store.dispatch('conversation/setUserLastSeen'); + } + this.setUnreadView(); + }); bus.$on(ON_UNREAD_MESSAGE_CLICK, () => { this.replaceRoute('messages').then(() => this.unsetUnreadView()); }); @@ -175,6 +181,7 @@ export default { }, setUnreadView() { const { unreadMessageCount } = this; + if (this.isIFrame && unreadMessageCount > 0 && !this.isWidgetOpen) { this.replaceRoute('unread-messages').then(() => { this.setIframeHeight(true); diff --git a/app/javascript/widget/store/modules/conversation/actions.js b/app/javascript/widget/store/modules/conversation/actions.js index c915ea6e7..ae06afa9c 100644 --- a/app/javascript/widget/store/modules/conversation/actions.js +++ b/app/javascript/widget/store/modules/conversation/actions.js @@ -84,8 +84,12 @@ export const actions = { fetchOldConversations: async ({ commit }, { before } = {}) => { try { commit('setConversationListLoading', true); - const { data } = await getMessagesAPI({ before }); - const formattedMessages = getNonDeletedMessages({ messages: data }); + const { + data: { payload, meta }, + } = await getMessagesAPI({ before }); + const { contact_last_seen_at: lastSeen } = meta; + const formattedMessages = getNonDeletedMessages({ messages: payload }); + commit('conversation/setMetaUserLastSeenAt', lastSeen, { root: true }); commit('setMessagesInConversation', formattedMessages); commit('setConversationListLoading', false); } catch (error) { diff --git a/app/javascript/widget/store/modules/specs/conversation/actions.spec.js b/app/javascript/widget/store/modules/specs/conversation/actions.spec.js index cfbb0ec66..82e03d38a 100644 --- a/app/javascript/widget/store/modules/specs/conversation/actions.spec.js +++ b/app/javascript/widget/store/modules/specs/conversation/actions.spec.js @@ -181,22 +181,28 @@ describe('#actions', () => { describe('#fetchOldConversations', () => { it('sends correct actions', async () => { API.get.mockResolvedValue({ - data: [ - { - id: 1, - text: 'hey', - content_attributes: {}, + data: { + payload: [ + { + id: 1, + text: 'hey', + content_attributes: {}, + }, + { + id: 2, + text: 'welcome', + content_attributes: { deleted: true }, + }, + ], + meta: { + contact_last_seen_at: 1466424490, }, - { - id: 2, - text: 'welcome', - content_attributes: { deleted: true }, - }, - ], + }, }); await actions.fetchOldConversations({ commit }, {}); expect(commit.mock.calls).toEqual([ ['setConversationListLoading', true], + ['conversation/setMetaUserLastSeenAt', 1466424490, { root: true }], [ 'setMessagesInConversation', [ diff --git a/app/views/api/v1/widget/messages/index.json.jbuilder b/app/views/api/v1/widget/messages/index.json.jbuilder index a0462b545..d7a441b84 100644 --- a/app/views/api/v1/widget/messages/index.json.jbuilder +++ b/app/views/api/v1/widget/messages/index.json.jbuilder @@ -1,11 +1,16 @@ -json.array! @messages do |message| - json.id message.id - json.content message.content - json.message_type message.message_type_before_type_cast - json.content_type message.content_type - json.content_attributes message.content_attributes - json.created_at message.created_at.to_i - json.conversation_id message.conversation.display_id - json.attachments message.attachments.map(&:push_event_data) if message.attachments.present? - json.sender message.sender.push_event_data if message.sender +json.payload do + json.array! @messages do |message| + json.id message.id + json.content message.content + json.message_type message.message_type_before_type_cast + json.content_type message.content_type + json.content_attributes message.content_attributes + json.created_at message.created_at.to_i + json.conversation_id message.conversation.display_id + json.attachments message.attachments.map(&:push_event_data) if message.attachments.present? + json.sender message.sender.push_event_data if message.sender + end +end +json.meta do + json.contact_last_seen_at @conversation.contact_last_seen_at.to_i end diff --git a/spec/controllers/api/v1/widget/messages_controller_spec.rb b/spec/controllers/api/v1/widget/messages_controller_spec.rb index 983f6f132..f203f11c8 100644 --- a/spec/controllers/api/v1/widget/messages_controller_spec.rb +++ b/spec/controllers/api/v1/widget/messages_controller_spec.rb @@ -23,9 +23,8 @@ RSpec.describe '/api/v1/widget/messages', type: :request do expect(response).to have_http_status(:success) json_response = JSON.parse(response.body) - # 2 messages created + 2 messages by the email hook - expect(json_response.length).to eq(4) + expect(json_response['payload'].length).to eq(4) end end end From 3cd1616df624f25205c338398466e978fcee0db9 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 31 Mar 2022 20:22:52 +0530 Subject: [PATCH 004/177] fix: Fix agent name in Twitter channel private note acting as a link (#4326) --- .../widgets/conversation/Message.vue | 6 +++- .../shared/helpers/MessageFormatter.js | 5 ++-- .../helpers/specs/MessageFormatter.spec.js | 30 +++++++++++++++++-- .../shared/mixins/messageFormatterMixin.js | 8 +++-- 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/app/javascript/dashboard/components/widgets/conversation/Message.vue b/app/javascript/dashboard/components/widgets/conversation/Message.vue index 51d82ff8c..4035a62d0 100644 --- a/app/javascript/dashboard/components/widgets/conversation/Message.vue +++ b/app/javascript/dashboard/components/widgets/conversation/Message.vue @@ -207,7 +207,11 @@ export default { } } return ( - this.formatMessage(this.data.content, this.isATweet) + botMessageContent + this.formatMessage( + this.data.content, + this.isATweet, + this.data.private + ) + botMessageContent ); }, contentAttributes() { diff --git a/app/javascript/shared/helpers/MessageFormatter.js b/app/javascript/shared/helpers/MessageFormatter.js index 3bc89ec2f..260436da4 100644 --- a/app/javascript/shared/helpers/MessageFormatter.js +++ b/app/javascript/shared/helpers/MessageFormatter.js @@ -13,8 +13,9 @@ const TWITTER_HASH_REPLACEMENT = const USER_MENTIONS_REGEX = /mention:\/\/(user|team)\/(\d+)\/(.+)/gm; class MessageFormatter { - constructor(message, isATweet = false) { + constructor(message, isATweet = false, isAPrivateNote = false) { this.message = DOMPurify.sanitize(escapeHtml(message || '')); + this.isAPrivateNote = isAPrivateNote; this.isATweet = isATweet; this.marked = marked; @@ -35,7 +36,7 @@ class MessageFormatter { } formatMessage() { - if (this.isATweet) { + if (this.isATweet && !this.isAPrivateNote) { const withUserName = this.message.replace( TWITTER_USERNAME_REGEX, TWITTER_USERNAME_REPLACEMENT diff --git a/app/javascript/shared/helpers/specs/MessageFormatter.spec.js b/app/javascript/shared/helpers/specs/MessageFormatter.spec.js index 4e13fe832..1f2209b71 100644 --- a/app/javascript/shared/helpers/specs/MessageFormatter.spec.js +++ b/app/javascript/shared/helpers/specs/MessageFormatter.spec.js @@ -36,19 +36,45 @@ describe('#MessageFormatter', () => { it('should add links to @mentions', () => { const message = '@chatwootapp is an opensource tool thanks @longnonexistenttwitterusername'; - expect(new MessageFormatter(message, true).formattedMessage).toMatch( + expect( + new MessageFormatter(message, true, false).formattedMessage + ).toMatch( '

@chatwootapp is an opensource tool thanks @longnonexistenttwitterusername

' ); }); it('should add links to #tags', () => { const message = '#chatwootapp is an opensource tool'; - expect(new MessageFormatter(message, true).formattedMessage).toMatch( + expect( + new MessageFormatter(message, true, false).formattedMessage + ).toMatch( '

#chatwootapp is an opensource tool

' ); }); }); + describe('private notes', () => { + it('should return the same string if not tags or @mentions', () => { + const message = 'Chatwoot is an opensource tool'; + expect(new MessageFormatter(message).formattedMessage).toMatch(message); + }); + + it('should add links to @mentions', () => { + const message = + '@chatwootapp is an opensource tool thanks @longnonexistenttwitterusername'; + expect( + new MessageFormatter(message, false, true).formattedMessage + ).toMatch(message); + }); + + it('should add links to #tags', () => { + const message = '#chatwootapp is an opensource tool'; + expect( + new MessageFormatter(message, false, true).formattedMessage + ).toMatch(message); + }); + }); + describe('plain text content', () => { it('returns the plain text without HTML', () => { const message = diff --git a/app/javascript/shared/mixins/messageFormatterMixin.js b/app/javascript/shared/mixins/messageFormatterMixin.js index 21096409d..7767e7c76 100644 --- a/app/javascript/shared/mixins/messageFormatterMixin.js +++ b/app/javascript/shared/mixins/messageFormatterMixin.js @@ -3,8 +3,12 @@ import DOMPurify from 'dompurify'; export default { methods: { - formatMessage(message, isATweet) { - const messageFormatter = new MessageFormatter(message, isATweet); + formatMessage(message, isATweet, isAPrivateNote) { + const messageFormatter = new MessageFormatter( + message, + isATweet, + isAPrivateNote + ); return messageFormatter.formattedMessage; }, getPlainText(message, isATweet) { From cb23ff53bf7f1b190223b13aedbfefcb7c4ec85f Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 31 Mar 2022 21:20:37 +0530 Subject: [PATCH 005/177] fix: Reset Captcha if account signup is an error (#4279) Co-authored-by: Pranav Raj S --- .../conversation/AvailabilityStatusBadge.vue | 2 +- .../i18n/locale/en/setNewPassword.json | 3 ++ .../dashboard/routes/auth/Signup.vue | 32 +++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/app/javascript/dashboard/components/widgets/conversation/AvailabilityStatusBadge.vue b/app/javascript/dashboard/components/widgets/conversation/AvailabilityStatusBadge.vue index 588bbef82..766166fd3 100644 --- a/app/javascript/dashboard/components/widgets/conversation/AvailabilityStatusBadge.vue +++ b/app/javascript/dashboard/components/widgets/conversation/AvailabilityStatusBadge.vue @@ -23,7 +23,7 @@ export default { background: var(--s-500); } &__busy { - background: var(--y-700); + background: var(--y-400); } } diff --git a/app/javascript/dashboard/i18n/locale/en/setNewPassword.json b/app/javascript/dashboard/i18n/locale/en/setNewPassword.json index 94a3fd2e1..ec2d94744 100644 --- a/app/javascript/dashboard/i18n/locale/en/setNewPassword.json +++ b/app/javascript/dashboard/i18n/locale/en/setNewPassword.json @@ -15,6 +15,9 @@ "SUCCESS_MESSAGE": "Successfully changed the password", "ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later" }, + "CAPTCHA": { + "ERROR": "Verification expired. Please solve captcha again." + }, "SUBMIT": "Submit" } } diff --git a/app/javascript/dashboard/routes/auth/Signup.vue b/app/javascript/dashboard/routes/auth/Signup.vue index 36b63662d..4f5aab8bd 100644 --- a/app/javascript/dashboard/routes/auth/Signup.vue +++ b/app/javascript/dashboard/routes/auth/Signup.vue @@ -77,9 +77,17 @@ />
+ + {{ $t('SET_NEW_PASSWORD.CAPTCHA.ERROR') }} +
From c397fe1964a2e139d29dc1273f7673f2a7fd8980 Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Thu, 31 Mar 2022 21:21:25 +0530 Subject: [PATCH 006/177] fix: Update the URL for POST inbox_members (#4354) --- swagger/paths/index.yml | 10 ++- swagger/swagger.json | 133 +++++++++++++++++++++------------------- 2 files changed, 77 insertions(+), 66 deletions(-) diff --git a/swagger/paths/index.yml b/swagger/paths/index.yml index eeb2539f1..279ebb7bc 100644 --- a/swagger/paths/index.yml +++ b/swagger/paths/index.yml @@ -253,13 +253,17 @@ - $ref: '#/parameters/inbox_id' get: $ref: ./application/inboxes/inbox_members/show.yml - post: - $ref: ./application/inboxes/inbox_members/create.yml patch: $ref: ./application/inboxes/inbox_members/update.yml delete: $ref: ./application/inboxes/inbox_members/delete.yml +/api/v1/accounts/{account_id}/inbox_members: + parameters: + - $ref: '#/parameters/account_id' + post: + $ref: ./application/inboxes/inbox_members/create.yml + # Messages @@ -422,4 +426,4 @@ type: string description: The numeric ID of the user get: - $ref: './application/reports/conversation/agent.yml' \ No newline at end of file + $ref: './application/reports/conversation/agent.yml' diff --git a/swagger/swagger.json b/swagger/swagger.json index b17f9c586..4de4db49d 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -2803,69 +2803,6 @@ } } }, - "post": { - "tags": [ - "Inbox" - ], - "operationId": "add-new-agent-to-inbox", - "summary": "Add a New Agent", - "description": "Add a new Agent to Inbox", - "security": [ - { - "userApiKey": [ - - ] - } - ], - "parameters": [ - { - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "object", - "required": [ - "inbox_id", - "user_ids" - ], - "properties": { - "inbox_id": { - "type": "string", - "description": "The ID of the inbox" - }, - "user_ids": { - "type": "array", - "items": { - "type": "integer" - }, - "description": "IDs of users to be added to the inbox" - } - } - } - } - ], - "responses": { - "200": { - "description": "Success", - "schema": { - "type": "array", - "description": "Array of all active agents", - "items": { - "$ref": "#/definitions/agent" - } - } - }, - "404": { - "description": "Inbox not found" - }, - "403": { - "description": "Access denied" - }, - "422": { - "description": "User must exist" - } - } - }, "patch": { "tags": [ "Inbox" @@ -2986,6 +2923,76 @@ } } }, + "/api/v1/accounts/{account_id}/inbox_members": { + "parameters": [ + { + "$ref": "#/parameters/account_id" + } + ], + "post": { + "tags": [ + "Inbox" + ], + "operationId": "add-new-agent-to-inbox", + "summary": "Add a New Agent", + "description": "Add a new Agent to Inbox", + "security": [ + { + "userApiKey": [ + + ] + } + ], + "parameters": [ + { + "name": "data", + "in": "body", + "required": true, + "schema": { + "type": "object", + "required": [ + "inbox_id", + "user_ids" + ], + "properties": { + "inbox_id": { + "type": "string", + "description": "The ID of the inbox" + }, + "user_ids": { + "type": "array", + "items": { + "type": "integer" + }, + "description": "IDs of users to be added to the inbox" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "type": "array", + "description": "Array of all active agents", + "items": { + "$ref": "#/definitions/agent" + } + } + }, + "404": { + "description": "Inbox not found" + }, + "403": { + "description": "Access denied" + }, + "422": { + "description": "User must exist" + } + } + } + }, "/api/v1/accounts/{account_id}/conversations/{conversation_id}/messages": { "parameters": [ { From 3813b3b372d5dfd9e6a5b6c5ad403295ed322ad7 Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Fri, 1 Apr 2022 00:34:50 +0530 Subject: [PATCH 007/177] feat: make linux script installation non-interactive (#4355) This PR changes the LetsEncrypt behaviour to be non-interactive. Earlier, the installation flow was waiting for user input at the SSL cert generation stage. With this change, once the user confirms completes the initial selection, the installation can be completed unattended. --- deployment/setup_20.04.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/deployment/setup_20.04.sh b/deployment/setup_20.04.sh index ca8b0a06a..14a0b74f6 100644 --- a/deployment/setup_20.04.sh +++ b/deployment/setup_20.04.sh @@ -2,7 +2,7 @@ # Description: Chatwoot installation script # OS: Ubuntu 20.04 LTS / Ubuntu 20.10 -# Script Version: 0.7 +# Script Version: 0.8 # Run this script as root read -p 'Would you like to configure a domain and SSL for Chatwoot?(yes or no): ' configure_webserver @@ -12,6 +12,7 @@ then read -p 'Enter your sub-domain to be used for Chatwoot (chatwoot.domain.com for example) : ' domain_name echo -e "\nThis script will try to generate SSL certificates via LetsEncrypt and serve chatwoot at "https://$domain_name". Proceed further once you have pointed your DNS to the IP of the instance.\n" +read -p 'Enter the email LetsEncrypt can use to send reminders when your SSL certificate is up for renewal: ' le_email read -p 'Do you wish to proceed? (yes or no): ' exit_true if [ $exit_true == "no" ] then @@ -138,7 +139,7 @@ else curl https://ssl-config.mozilla.org/ffdhe4096.txt >> /etc/ssl/dhparam wget https://raw.githubusercontent.com/chatwoot/chatwoot/develop/deployment/nginx_chatwoot.conf cp nginx_chatwoot.conf /etc/nginx/sites-available/nginx_chatwoot.conf -certbot certonly --nginx -d $domain_name +certbot certonly --non-interactive --agree-tos --nginx -m $le_email -d $domain_name sed -i "s/chatwoot.domain.com/$domain_name/g" /etc/nginx/sites-available/nginx_chatwoot.conf ln -s /etc/nginx/sites-available/nginx_chatwoot.conf /etc/nginx/sites-enabled/nginx_chatwoot.conf systemctl restart nginx From caee9535f1f99168a19a88187c170e0b695179b6 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Fri, 1 Apr 2022 20:59:03 +0530 Subject: [PATCH 008/177] feat: Support Dark mode for the widget (#4137) Co-authored-by: Pranav Raj S --- app/controllers/widget_tests_controller.rb | 5 ++ .../dashboard/settings/agents/Index.vue | 4 +- app/javascript/packs/sdk.js | 9 +++- app/javascript/sdk/IFrameHelper.js | 1 + app/javascript/sdk/constants.js | 1 + app/javascript/sdk/settingsHelper.js | 5 +- app/javascript/shared/components/ChatCard.vue | 17 +++--- app/javascript/shared/components/ChatForm.vue | 21 ++++++-- .../shared/components/ChatOption.vue | 2 +- .../shared/components/ChatOptions.vue | 10 ++-- .../components/CustomerSatisfaction.vue | 28 +++++++--- .../shared/components/DateSeparator.vue | 9 +++- .../shared/components/ResizableTextArea.vue | 1 - .../components/specs/DateSeparator.spec.js | 53 +++++++++++++++---- .../__snapshots__/DateSeparator.spec.js.snap | 4 +- .../assets/scss/views/_conversation.scss | 3 -- .../widget/components/AgentMessage.vue | 12 +++-- .../widget/components/AgentMessageBubble.vue | 10 +++- .../widget/components/AgentTypingBubble.vue | 7 ++- .../widget/components/ChatHeader.vue | 28 +++++++--- .../widget/components/ChatHeaderExpanded.vue | 17 ++++-- .../widget/components/ChatInputWrap.vue | 25 ++++++--- .../widget/components/Form/Input.vue | 39 +++++++++----- .../widget/components/Form/TextArea.vue | 37 ++++++++----- .../widget/components/HeaderActions.vue | 20 +++++-- .../widget/components/PreChat/Form.vue | 6 ++- .../widget/components/TeamAvailability.vue | 12 +++-- .../widget/components/UnreadMessage.vue | 9 +++- .../components/layouts/ViewWithHeader.vue | 13 +++-- .../widget/components/template/Article.vue | 24 +++++++-- .../widget/components/template/EmailInput.vue | 17 +++++- app/javascript/widget/mixins/darkModeMixin.js | 15 ++++++ .../widget/mixins/specs/darkModeMixin.spec.js | 41 ++++++++++++++ .../widget/store/modules/appConfig.js | 12 ++++- app/views/widget_tests/index.html.erb | 1 + tailwind.config.js | 6 ++- 36 files changed, 411 insertions(+), 113 deletions(-) create mode 100644 app/javascript/widget/mixins/darkModeMixin.js create mode 100644 app/javascript/widget/mixins/specs/darkModeMixin.spec.js diff --git a/app/controllers/widget_tests_controller.rb b/app/controllers/widget_tests_controller.rb index fff47d907..6d6742cf4 100644 --- a/app/controllers/widget_tests_controller.rb +++ b/app/controllers/widget_tests_controller.rb @@ -3,6 +3,7 @@ class WidgetTestsController < ActionController::Base before_action :ensure_widget_position before_action :ensure_widget_type before_action :ensure_widget_style + before_action :ensure_dark_mode def index render @@ -14,6 +15,10 @@ class WidgetTestsController < ActionController::Base @widget_style = params[:widget_style] || 'standard' end + def ensure_dark_mode + @dark_mode = params[:dark_mode] || 'light' + end + def ensure_widget_position @widget_position = params[:position] || 'left' end diff --git a/app/javascript/dashboard/routes/dashboard/settings/agents/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/agents/Index.vue index c3a75b0f6..5714582d5 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/agents/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/agents/Index.vue @@ -35,7 +35,9 @@ - {{ agent.name }} + + {{ agent.name }} + {{ agent.email }} diff --git a/app/javascript/packs/sdk.js b/app/javascript/packs/sdk.js index 76e1a2cd8..b77b682dd 100755 --- a/app/javascript/packs/sdk.js +++ b/app/javascript/packs/sdk.js @@ -1,6 +1,10 @@ import Cookies from 'js-cookie'; import { IFrameHelper } from '../sdk/IFrameHelper'; -import { getBubbleView } from '../sdk/settingsHelper'; +import { + getBubbleView, + getDarkMode, + getWidgetStyle, +} from '../sdk/settingsHelper'; import { computeHashForUserData, getUserCookieName, @@ -24,8 +28,9 @@ const runSDK = ({ baseUrl, websiteToken }) => { type: getBubbleView(chatwootSettings.type), launcherTitle: chatwootSettings.launcherTitle || '', showPopoutButton: chatwootSettings.showPopoutButton || false, - widgetStyle: chatwootSettings.widgetStyle || 'standard', + widgetStyle: getWidgetStyle(chatwootSettings.widgetStyle) || 'standard', resetTriggered: false, + darkMode: getDarkMode(chatwootSettings.darkMode), toggle(state) { IFrameHelper.events.toggleBubble(state); diff --git a/app/javascript/sdk/IFrameHelper.js b/app/javascript/sdk/IFrameHelper.js index df36d8e0d..3bc0973fe 100644 --- a/app/javascript/sdk/IFrameHelper.js +++ b/app/javascript/sdk/IFrameHelper.js @@ -145,6 +145,7 @@ export const IFrameHelper = { hideMessageBubble: window.$chatwoot.hideMessageBubble, showPopoutButton: window.$chatwoot.showPopoutButton, widgetStyle: window.$chatwoot.widgetStyle, + darkMode: window.$chatwoot.darkMode, }); IFrameHelper.onLoad({ widgetColor: message.config.channelConfig.widgetColor, diff --git a/app/javascript/sdk/constants.js b/app/javascript/sdk/constants.js index 52faf1540..7bf84d430 100644 --- a/app/javascript/sdk/constants.js +++ b/app/javascript/sdk/constants.js @@ -1,2 +1,3 @@ export const BUBBLE_DESIGN = ['standard', 'expanded_bubble']; export const WIDGET_DESIGN = ['standard', 'flat']; +export const DARK_MODE = ['light', 'auto']; diff --git a/app/javascript/sdk/settingsHelper.js b/app/javascript/sdk/settingsHelper.js index 4e256b53a..657db1fe0 100644 --- a/app/javascript/sdk/settingsHelper.js +++ b/app/javascript/sdk/settingsHelper.js @@ -1,4 +1,4 @@ -import { BUBBLE_DESIGN, WIDGET_DESIGN } from './constants'; +import { BUBBLE_DESIGN, DARK_MODE, WIDGET_DESIGN } from './constants'; export const getBubbleView = type => BUBBLE_DESIGN.includes(type) ? type : BUBBLE_DESIGN[0]; @@ -9,3 +9,6 @@ export const getWidgetStyle = style => WIDGET_DESIGN.includes(style) ? style : WIDGET_DESIGN[0]; export const isFlatWidgetStyle = style => style === 'flat'; + +export const getDarkMode = darkMode => + DARK_MODE.includes(darkMode) ? darkMode : DARK_MODE[0]; diff --git a/app/javascript/shared/components/ChatCard.vue b/app/javascript/shared/components/ChatCard.vue index 4e856bb6b..eb6e13364 100644 --- a/app/javascript/shared/components/ChatCard.vue +++ b/app/javascript/shared/components/ChatCard.vue @@ -1,11 +1,14 @@ - diff --git a/app/javascript/dashboard/components/ui/Switch.vue b/app/javascript/dashboard/components/ui/Switch.vue index 98755a91b..b0bc32881 100644 --- a/app/javascript/dashboard/components/ui/Switch.vue +++ b/app/javascript/dashboard/components/ui/Switch.vue @@ -1,54 +1,66 @@ + diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue index 5157c4dfc..f028933cb 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue @@ -34,9 +34,9 @@ {{ automation.name }} {{ automation.description }} - {{ readableTime(automation.created_on) }} @@ -131,12 +131,11 @@ import AddAutomationRule from './AddAutomationRule.vue'; import EditAutomationRule from './EditAutomationRule.vue'; import alertMixin from 'shared/mixins/alertMixin'; import timeMixin from 'dashboard/mixins/time'; -import ToggleButton from 'dashboard/components/buttons/ToggleButton'; + export default { components: { AddAutomationRule, EditAutomationRule, - ToggleButton, }, mixins: [alertMixin, timeMixin], data() { diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/FilterSelector.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/FilterSelector.vue index 87a422be0..cadf4078e 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/FilterSelector.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/FilterSelector.vue @@ -62,7 +62,9 @@ />
- {{ $t('REPORT.BUSINESS_HOURS') }} + + {{ $t('REPORT.BUSINESS_HOURS') }} + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue index 89d1fd4a9..0bfeae8a1 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue @@ -146,9 +146,11 @@ />
- {{ $t('REPORT.BUSINESS_HOURS') }} - - + + {{ $t('REPORT.BUSINESS_HOURS') }} + + +
From f1ae09d21bf52c7516bf5297832db3a27215d6fe Mon Sep 17 00:00:00 2001 From: "Aswin Dev P.S" Date: Mon, 11 Apr 2022 12:35:03 +0530 Subject: [PATCH 036/177] fix: SMTP shouldn't be configurable without IMAP enabled (#4428) * fix: Configure SMTP only if IMAP is enabled --- .../dashboard/components/SettingsSection.vue | 12 ++++++++++++ .../dashboard/i18n/locale/en/inboxMgmt.json | 4 +++- .../routes/dashboard/settings/inbox/ImapSettings.vue | 8 +++++++- .../routes/dashboard/settings/inbox/Settings.vue | 2 +- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/app/javascript/dashboard/components/SettingsSection.vue b/app/javascript/dashboard/components/SettingsSection.vue index 26caff02c..e700e9467 100644 --- a/app/javascript/dashboard/components/SettingsSection.vue +++ b/app/javascript/dashboard/components/SettingsSection.vue @@ -7,6 +7,10 @@

{{ subTitle }}

+

+ {{ $t('INBOX_MGMT.NOTE') }} + {{ note }} +

@@ -25,6 +29,10 @@ export default { type: String, required: true, }, + note: { + type: String, + default: '', + }, }, }; @@ -46,5 +54,9 @@ export default { .title--section { padding-right: var(--space-large); } + + .note { + font-weight: var(--font-weight-bold); + } } diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index 83a28de43..bf2652e09 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -470,6 +470,7 @@ "IMAP": { "TITLE": "IMAP", "SUBTITLE": "Set your IMAP details", + "NOTE_TEXT": "To enable SMTP, please configure IMAP.", "UPDATE": "Update IMAP settings", "TOGGLE_AVAILABILITY": "Enable IMAP configuration for this inbox", "TOGGLE_HELP": "Enabling IMAP will help the user to recieve email", @@ -529,6 +530,7 @@ "SSL_TLS": "SSL/TLS", "START_TLS": "STARTTLS", "OPEN_SSL_VERIFY_MODE": "Open SSL Verify Mode" - } + }, + "NOTE": "Note: " } } diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/ImapSettings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/ImapSettings.vue index 801e6fa98..c404c2872 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/ImapSettings.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/ImapSettings.vue @@ -3,6 +3,7 @@
- +
From fb3ed29c90cc6a31b1e8737f3ef0b98bc93e90e0 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Mon, 11 Apr 2022 14:06:44 +0530 Subject: [PATCH 037/177] chore: Add missing test cases in `conversations_controller_spec` (#4438) --- .../api/v1/widget/conversations/create.json.jbuilder | 1 + .../api/v1/widget/conversations_controller_spec.rb | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/views/api/v1/widget/conversations/create.json.jbuilder b/app/views/api/v1/widget/conversations/create.json.jbuilder index 2576af592..17585c2f8 100644 --- a/app/views/api/v1/widget/conversations/create.json.jbuilder +++ b/app/views/api/v1/widget/conversations/create.json.jbuilder @@ -7,4 +7,5 @@ json.messages do json.partial! 'api/v1/models/widget_message', resource: message end end +json.custom_attributes @conversation.custom_attributes json.contact @conversation.contact diff --git a/spec/controllers/api/v1/widget/conversations_controller_spec.rb b/spec/controllers/api/v1/widget/conversations_controller_spec.rb index 0d3edb0dd..88f41dea0 100644 --- a/spec/controllers/api/v1/widget/conversations_controller_spec.rb +++ b/spec/controllers/api/v1/widget/conversations_controller_spec.rb @@ -54,19 +54,22 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do website_token: web_widget.website_token, contact: { name: 'contact-name', - email: 'contact-email@chatwoot.com' + email: 'contact-email@chatwoot.com', + phone_number: '+919745313456' }, message: { content: 'This is a test message' - } + }, + custom_attributes: { order_id: '12345' } }, as: :json expect(response).to have_http_status(:success) json_response = JSON.parse(response.body) - expect(json_response['id']).not_to eq nil expect(json_response['contact']['email']).to eq 'contact-email@chatwoot.com' + expect(json_response['contact']['phone_number']).to eq '+919745313456' + expect(json_response['custom_attributes']['order_id']).to eq '12345' expect(json_response['messages'][0]['content']).to eq 'This is a test message' end end From 8622740161237398bcafd7384ecac262dfd01940 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Apr 2022 15:10:37 +0530 Subject: [PATCH 038/177] chore(deps): bump moment from 2.29.1 to 2.29.2 (#4434) Bumps [moment](https://github.com/moment/moment) from 2.29.1 to 2.29.2. - [Release notes](https://github.com/moment/moment/releases) - [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md) - [Commits](https://github.com/moment/moment/compare/2.29.1...2.29.2) --- updated-dependencies: - dependency-name: moment dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 9acb20d6a..b4c4e505c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10167,9 +10167,9 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== moment@^2.10.2, moment@^2.27.0: - version "2.29.1" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" - integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== + version "2.29.2" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.2.tgz#00910c60b20843bcba52d37d58c628b47b1f20e4" + integrity sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg== move-concurrently@^1.0.1: version "1.0.1" From 9b5eb98c5924201a3aae8e98c62b20b63284eb24 Mon Sep 17 00:00:00 2001 From: "Aswin Dev P.S" Date: Mon, 11 Apr 2022 15:43:05 +0530 Subject: [PATCH 039/177] feat: Support additional authentication mechanisms for SMTP (#4431) * Support additional authentication mechanisms for SMTP --- app/helpers/api/v1/inboxes_helper.rb | 6 +++- .../dashboard/i18n/locale/en/inboxMgmt.json | 3 +- .../dashboard/settings/inbox/SmtpSettings.vue | 23 +++++++++++++++ app/models/channel/email.rb | 2 +- app/views/api/v1/models/_inbox.json.jbuilder | 1 + .../v1/accounts/inboxes_controller_spec.rb | 28 +++++++++++++++++++ 6 files changed, 60 insertions(+), 3 deletions(-) diff --git a/app/helpers/api/v1/inboxes_helper.rb b/app/helpers/api/v1/inboxes_helper.rb index 8cdf8d987..7f3b68953 100644 --- a/app/helpers/api/v1/inboxes_helper.rb +++ b/app/helpers/api/v1/inboxes_helper.rb @@ -29,8 +29,12 @@ module Api::V1::InboxesHelper smtp = Net::SMTP.new(channel_data[:smtp_address], channel_data[:smtp_port]) set_smtp_encryption(channel_data, smtp) + check_smtp_connection(channel_data, smtp) + end - smtp.start(channel_data[:smtp_domain], channel_data[:smtp_email], channel_data[:smtp_password], :login) + def check_smtp_connection(channel_data, smtp) + smtp.start(channel_data[:smtp_domain], channel_data[:smtp_email], channel_data[:smtp_password], + channel_data[:smtp_authentication]&.to_sym || :login) smtp.finish unless smtp&.nil? end diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index bf2652e09..b2a054ebc 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -529,7 +529,8 @@ "ENCRYPTION": "Encryption", "SSL_TLS": "SSL/TLS", "START_TLS": "STARTTLS", - "OPEN_SSL_VERIFY_MODE": "Open SSL Verify Mode" + "OPEN_SSL_VERIFY_MODE": "Open SSL Verify Mode", + "AUTH_MECHANISM": "Authentication" }, "NOTE": "Note: " } diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/SmtpSettings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/SmtpSettings.vue index 21a8955ef..92b9a5446 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/SmtpSettings.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/SmtpSettings.vue @@ -69,6 +69,13 @@ :options="openSSLVerifyModes" :action="handleSSLModeChange" /> +
Date: Mon, 11 Apr 2022 16:43:09 +0530 Subject: [PATCH 040/177] chore: Swagger doc for Conversation meta API (#4394) fixes: https://github.com/chatwoot/chatwoot/issues/4327 --- .../resource/extension/conversation/list.yml | 2 + swagger/parameters/index.yml | 12 +- .../paths/application/conversation/index.yml | 24 ++- .../paths/application/conversation/meta.yml | 53 ++++++ swagger/paths/index.yml | 2 + swagger/swagger.json | 159 +++++++++++++++--- 6 files changed, 212 insertions(+), 40 deletions(-) create mode 100644 swagger/paths/application/conversation/meta.yml diff --git a/swagger/definitions/resource/extension/conversation/list.yml b/swagger/definitions/resource/extension/conversation/list.yml index b1a17baee..426902d4f 100644 --- a/swagger/definitions/resource/extension/conversation/list.yml +++ b/swagger/definitions/resource/extension/conversation/list.yml @@ -10,6 +10,8 @@ properties: type: number unassigned_count: type: number + assigned_count: + type: number all_count: type: number payload: diff --git a/swagger/parameters/index.yml b/swagger/parameters/index.yml index 89b685a41..d69640369 100644 --- a/swagger/parameters/index.yml +++ b/swagger/parameters/index.yml @@ -16,24 +16,24 @@ hook_id: source_id: $ref: ./source_id.yml +contact_sort_param: + $ref: ./contact_sort.yml + conversation_id: $ref: ./conversation_id.yml +custom_filter_id: + $ref: ./custom_filter_id.yml + message_id: $ref: ./message_id.yml -contact_sort_param: - $ref: ./contact_sort.yml - page: $ref: ./page.yml platform_user_id: $ref: ./platform_user_id.yml -custom_filter_id: - $ref: ./custom_filter_id.yml - report_type: $ref: ./report_type.yml diff --git a/swagger/paths/application/conversation/index.yml b/swagger/paths/application/conversation/index.yml index 8041a7af6..54650f4d2 100644 --- a/swagger/paths/application/conversation/index.yml +++ b/swagger/paths/application/conversation/index.yml @@ -12,17 +12,22 @@ get: in: query type: string enum: ['me', 'unassigned', 'all', 'assigned'] - required: true + default: 'all' + description: Filter conversations by assignee type. - name: status in: query type: string - enum: ['open', 'resolved', 'pending'] - required: true - - name: page + enum: ['open', 'resolved', 'pending', 'snoozed'] + default: 'open' + description: Filter by conversation status. + - name: q + in: query + type: string + description: Filters conversations with messages containing the search term + - name: inbox_id in: query type: integer - required: true - - name: inbox_id + - name: team_id in: query type: integer - name: labels @@ -30,7 +35,12 @@ get: type: array items: type: string - + - name: page + in: query + type: integer + default: 1 + description: paginate through conversations + responses: 200: description: Success diff --git a/swagger/paths/application/conversation/meta.yml b/swagger/paths/application/conversation/meta.yml new file mode 100644 index 000000000..9bf32ef03 --- /dev/null +++ b/swagger/paths/application/conversation/meta.yml @@ -0,0 +1,53 @@ +parameters: + - $ref: '#/parameters/account_id' + +get: + tags: + - Conversations + operationId: conversationListMeta + description: Get open, unassigned and all Conversation counts + summary: Get Conversation Counts + parameters: + - name: status + in: query + type: string + enum: ['open', 'resolved', 'pending', 'snoozed'] + default: 'open' + description: Filter by conversation status. + - name: q + in: query + type: string + description: Filters conversations with messages containing the search term + - name: inbox_id + in: query + type: integer + - name: team_id + in: query + type: integer + - name: labels + in: query + type: array + items: + type: string + + responses: + 200: + description: Success + schema: + type: object + properties: + meta: + type: object + properties: + mine_count: + type: number + unassigned_count: + type: number + assigned_count: + type: number + all_count: + type: number + 400: + description: Bad Request Error + schema: + $ref: '#/definitions/bad_request_error' \ No newline at end of file diff --git a/swagger/paths/index.yml b/swagger/paths/index.yml index 87faec643..2175a3154 100644 --- a/swagger/paths/index.yml +++ b/swagger/paths/index.yml @@ -209,6 +209,8 @@ # Conversations +/api/v1/accounts/{account_id}/conversations/meta: + $ref: ./application/conversation/meta.yml /api/v1/accounts/{account_id}/conversations: $ref: ./application/conversation/index.yml /api/v1/accounts/{account_id}/conversations/filter: diff --git a/swagger/swagger.json b/swagger/swagger.json index b654a669b..c1266fcff 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -2135,6 +2135,93 @@ } } }, + "/api/v1/accounts/{account_id}/conversations/meta": { + "parameters": [ + { + "$ref": "#/parameters/account_id" + } + ], + "get": { + "tags": [ + "Conversations" + ], + "operationId": "conversationListMeta", + "description": "Get open, unassigned and all Conversation counts", + "summary": "Get Conversation Counts", + "parameters": [ + { + "name": "status", + "in": "query", + "type": "string", + "enum": [ + "open", + "resolved", + "pending", + "snoozed" + ], + "default": "open", + "description": "Filter by conversation status." + }, + { + "name": "q", + "in": "query", + "type": "string", + "description": "Filters conversations with messages containing the search term" + }, + { + "name": "inbox_id", + "in": "query", + "type": "integer" + }, + { + "name": "team_id", + "in": "query", + "type": "integer" + }, + { + "name": "labels", + "in": "query", + "type": "array", + "items": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "type": "object", + "properties": { + "meta": { + "type": "object", + "properties": { + "mine_count": { + "type": "number" + }, + "unassigned_count": { + "type": "number" + }, + "assigned_count": { + "type": "number" + }, + "all_count": { + "type": "number" + } + } + } + } + } + }, + "400": { + "description": "Bad Request Error", + "schema": { + "$ref": "#/definitions/bad_request_error" + } + } + } + } + }, "/api/v1/accounts/{account_id}/conversations": { "parameters": [ { @@ -2159,7 +2246,8 @@ "all", "assigned" ], - "required": true + "default": "all", + "description": "Filter conversations by assignee type." }, { "name": "status", @@ -2168,21 +2256,28 @@ "enum": [ "open", "resolved", - "pending" + "pending", + "snoozed" ], - "required": true + "default": "open", + "description": "Filter by conversation status." }, { - "name": "page", + "name": "q", "in": "query", - "type": "integer", - "required": true + "type": "string", + "description": "Filters conversations with messages containing the search term" }, { "name": "inbox_id", "in": "query", "type": "integer" }, + { + "name": "team_id", + "in": "query", + "type": "integer" + }, { "name": "labels", "in": "query", @@ -2190,6 +2285,13 @@ "items": { "type": "string" } + }, + { + "name": "page", + "in": "query", + "type": "integer", + "default": 1, + "description": "paginate through conversations" } ], "responses": { @@ -5314,6 +5416,9 @@ "unassigned_count": { "type": "number" }, + "assigned_count": { + "type": "number" + }, "all_count": { "type": "number" } @@ -5595,20 +5700,6 @@ "type": "string", "description": "Id of the session for which the conversation is created.\n\n\n\n Source Ids can be obtained through contactable inboxes API or via generated.

Website: Chatwoot generated string which can be obtained from webhook events.
Phone Channels(Twilio): Phone number in e164 format
Email Channels: Contact Email address
API Channel: Any Random String" }, - "conversation_id": { - "in": "path", - "name": "conversation_id", - "type": "integer", - "required": true, - "description": "The numeric ID of the conversation" - }, - "message_id": { - "in": "path", - "name": "message_id", - "type": "integer", - "required": true, - "description": "The numeric ID of the message" - }, "contact_sort_param": { "in": "query", "name": "sort", @@ -5626,6 +5717,27 @@ "required": false, "description": "The attribute by which list should be sorted" }, + "conversation_id": { + "in": "path", + "name": "conversation_id", + "type": "integer", + "required": true, + "description": "The numeric ID of the conversation" + }, + "custom_filter_id": { + "in": "path", + "name": "custom_filter_id", + "type": "integer", + "required": true, + "description": "The numeric ID of the custom filter" + }, + "message_id": { + "in": "path", + "name": "message_id", + "type": "integer", + "required": true, + "description": "The numeric ID of the message" + }, "page": { "in": "query", "name": "page", @@ -5641,13 +5753,6 @@ "required": true, "description": "The numeric ID of the user on the platform" }, - "custom_filter_id": { - "in": "path", - "name": "custom_filter_id", - "type": "integer", - "required": true, - "description": "The numeric ID of the custom filter" - }, "report_type": { "in": "query", "name": "report_type", From 3d164271a85b13486b72e40ead24052514febd5c Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Mon, 11 Apr 2022 17:27:28 +0530 Subject: [PATCH 041/177] fix: Yellow color shades are inconsistent (#4391) --- .../dashboard/assets/scss/_utility-helpers.scss | 4 ---- app/javascript/dashboard/components/ui/Banner.vue | 6 +++--- app/javascript/dashboard/components/ui/Label.vue | 2 +- app/javascript/dashboard/components/widgets/Thumbnail.vue | 2 +- .../widgets/conversation/AvailabilityStatusBadge.vue | 2 +- .../widgets/conversation/ConversationHeader.vue | 8 ++++++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/javascript/dashboard/assets/scss/_utility-helpers.scss b/app/javascript/dashboard/assets/scss/_utility-helpers.scss index 8a85dc6a5..abea0e28c 100644 --- a/app/javascript/dashboard/assets/scss/_utility-helpers.scss +++ b/app/javascript/dashboard/assets/scss/_utility-helpers.scss @@ -51,10 +51,6 @@ background-color: var(--white); } -.text-y-800 { - color: var(--y-800); -} - .text-ellipsis { overflow: hidden; text-overflow: ellipsis; diff --git a/app/javascript/dashboard/components/ui/Banner.vue b/app/javascript/dashboard/components/ui/Banner.vue index c9d71bfaf..227b0a37e 100644 --- a/app/javascript/dashboard/components/ui/Banner.vue +++ b/app/javascript/dashboard/components/ui/Banner.vue @@ -112,10 +112,10 @@ export default { } &.warning { - background: var(--y-800); - color: var(--s-600); + background: var(--y-600); + color: var(--y-500); a { - color: var(--s-600); + color: var(--y-500); } } diff --git a/app/javascript/dashboard/components/ui/Label.vue b/app/javascript/dashboard/components/ui/Label.vue index 83e55b040..504f6fc9e 100644 --- a/app/javascript/dashboard/components/ui/Label.vue +++ b/app/javascript/dashboard/components/ui/Label.vue @@ -157,7 +157,7 @@ export default { &.warning { background: var(--y-100); color: var(--y-900); - border: 1px solid var(--y-300); + border: 1px solid var(--y-200); a { color: var(--y-900); } diff --git a/app/javascript/dashboard/components/widgets/Thumbnail.vue b/app/javascript/dashboard/components/widgets/Thumbnail.vue index 5a4a69008..d12a927ef 100644 --- a/app/javascript/dashboard/components/widgets/Thumbnail.vue +++ b/app/javascript/dashboard/components/widgets/Thumbnail.vue @@ -209,7 +209,7 @@ export default { } .user-online-status--busy { - background: var(--y-700); + background: var(--y-500); } .user-online-status--offline { diff --git a/app/javascript/dashboard/components/widgets/conversation/AvailabilityStatusBadge.vue b/app/javascript/dashboard/components/widgets/conversation/AvailabilityStatusBadge.vue index 766166fd3..50bcab5ab 100644 --- a/app/javascript/dashboard/components/widgets/conversation/AvailabilityStatusBadge.vue +++ b/app/javascript/dashboard/components/widgets/conversation/AvailabilityStatusBadge.vue @@ -23,7 +23,7 @@ export default { background: var(--s-500); } &__busy { - background: var(--y-400); + background: var(--y-500); } } diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue index 89431eca3..c30f23c20 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue @@ -14,8 +14,8 @@ @@ -181,7 +181,11 @@ export default { .snoozed--display-text { font-weight: var(--font-weight-medium); - color: var(--y-900); + color: var(--y-600); } } + +.hmac-warning__icon { + color: var(--y-600); +} From 31cdc63e18f3ceda86ae4014469ac7ac70a09fe6 Mon Sep 17 00:00:00 2001 From: "Aswin Dev P.S" Date: Mon, 11 Apr 2022 19:37:20 +0530 Subject: [PATCH 042/177] fix: Remove IMAP and SMTP email validation (#4435) * Remove IMAP and SMTP email validation * Rename imap_email & smtp_email columns to imap_login & smtp_login respectively. * Use channel email domain if inbound email domain not present --- app/helpers/api/v1/inboxes_helper.rb | 4 ++-- .../dashboard/i18n/locale/en/inboxMgmt.json | 12 +++++----- .../dashboard/settings/inbox/ImapSettings.vue | 22 +++++++++---------- .../dashboard/settings/inbox/SmtpSettings.vue | 22 +++++++++---------- app/jobs/inboxes/fetch_imap_emails_job.rb | 2 +- app/mailers/conversation_reply_mailer.rb | 4 ++-- .../conversation_reply_mailer_helper.rb | 14 +++++++++--- app/models/channel/email.rb | 8 +++---- app/views/api/v1/models/_inbox.json.jbuilder | 4 ++-- config/schedule.yml | 2 +- ...ename_imap_email_and_smtp_email_columns.rb | 6 +++++ db/schema.rb | 6 ++--- .../v1/accounts/inboxes_controller_spec.rb | 6 ++--- .../fetch_imap_email_inboxes_job_spec.rb | 2 +- .../inboxes/fetch_imap_emails_job_spec.rb | 2 +- spec/mailboxes/imap/imap_mailbox_spec.rb | 2 +- .../mailers/conversation_reply_mailer_spec.rb | 21 +++++++++++++++++- 17 files changed, 86 insertions(+), 53 deletions(-) create mode 100644 db/migrate/20220409044943_rename_imap_email_and_smtp_email_columns.rb diff --git a/app/helpers/api/v1/inboxes_helper.rb b/app/helpers/api/v1/inboxes_helper.rb index 7f3b68953..56fb79908 100644 --- a/app/helpers/api/v1/inboxes_helper.rb +++ b/app/helpers/api/v1/inboxes_helper.rb @@ -14,7 +14,7 @@ module Api::V1::InboxesHelper Mail.defaults do retriever_method :imap, { address: channel_data[:imap_address], port: channel_data[:imap_port], - user_name: channel_data[:imap_email], + user_name: channel_data[:imap_login], password: channel_data[:imap_password], enable_ssl: channel_data[:imap_enable_ssl] } end @@ -33,7 +33,7 @@ module Api::V1::InboxesHelper end def check_smtp_connection(channel_data, smtp) - smtp.start(channel_data[:smtp_domain], channel_data[:smtp_email], channel_data[:smtp_password], + smtp.start(channel_data[:smtp_domain], channel_data[:smtp_login], channel_data[:smtp_password], channel_data[:smtp_authentication]&.to_sym || :login) smtp.finish unless smtp&.nil? end diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index b2a054ebc..365fd92f9 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -486,9 +486,9 @@ "LABEL": "Port", "PLACE_HOLDER": "Port" }, - "EMAIL": { - "LABEL": "Email", - "PLACE_HOLDER": "Email" + "LOGIN": { + "LABEL": "Login", + "PLACE_HOLDER": "Login" }, "PASSWORD": { "LABEL": "Password", @@ -514,9 +514,9 @@ "LABEL": "Port", "PLACE_HOLDER": "Port" }, - "EMAIL": { - "LABEL": "Email", - "PLACE_HOLDER": "Email" + "LOGIN": { + "LABEL": "Login", + "PLACE_HOLDER": "Login" }, "PASSWORD": { "LABEL": "Password", diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/ImapSettings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/ImapSettings.vue index c404c2872..af2d5dd5c 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/ImapSettings.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/ImapSettings.vue @@ -34,12 +34,12 @@ @blur="$v.port.$touch" /> " + "" end def in_reply_to_email - conversation_reply_email_id || "" + conversation_reply_email_id || "" end def conversation_reply_email_id diff --git a/app/mailers/conversation_reply_mailer_helper.rb b/app/mailers/conversation_reply_mailer_helper.rb index f95150232..21bc9b939 100644 --- a/app/mailers/conversation_reply_mailer_helper.rb +++ b/app/mailers/conversation_reply_mailer_helper.rb @@ -26,7 +26,7 @@ module ConversationReplyMailerHelper smtp_settings = { address: @channel.smtp_address, port: @channel.smtp_port, - user_name: @channel.smtp_email, + user_name: @channel.smtp_login, password: @channel.smtp_password, domain: @channel.smtp_domain, tls: @channel.smtp_enable_ssl_tls, @@ -48,10 +48,18 @@ module ConversationReplyMailerHelper end def email_from - email_smtp_enabled ? @channel.smtp_email : from_email_with_name + email_smtp_enabled ? @channel.email : from_email_with_name end def email_reply_to - email_imap_enabled ? @channel.imap_email : reply_email + email_imap_enabled ? @channel.email : reply_email + end + + # Use channel email domain in case of account email domain is not set for custom message_id and in_reply_to + def channel_email_domain + return @account.inbound_email_domain if @account.inbound_email_domain.present? + + email = @inbox.channel.try(:email) + email.present? ? email.split('@').last : raise(StandardError, 'Channel email domain not present.') end end diff --git a/app/models/channel/email.rb b/app/models/channel/email.rb index 1c7f534a4..5b1cc0c91 100644 --- a/app/models/channel/email.rb +++ b/app/models/channel/email.rb @@ -6,19 +6,19 @@ # email :string not null # forward_to_email :string not null # imap_address :string default("") -# imap_email :string default("") # imap_enable_ssl :boolean default(TRUE) # imap_enabled :boolean default(FALSE) # imap_inbox_synced_at :datetime +# imap_login :string default("") # imap_password :string default("") # imap_port :integer default(0) # smtp_address :string default("") # smtp_authentication :string default("login") # smtp_domain :string default("") -# smtp_email :string default("") # smtp_enable_ssl_tls :boolean default(FALSE) # smtp_enable_starttls_auto :boolean default(TRUE) # smtp_enabled :boolean default(FALSE) +# smtp_login :string default("") # smtp_openssl_verify_mode :string default("none") # smtp_password :string default("") # smtp_port :integer default(0) @@ -37,8 +37,8 @@ class Channel::Email < ApplicationRecord include Reauthorizable self.table_name = 'channel_email' - EDITABLE_ATTRS = [:email, :imap_enabled, :imap_email, :imap_password, :imap_address, :imap_port, :imap_enable_ssl, :imap_inbox_synced_at, - :smtp_enabled, :smtp_email, :smtp_password, :smtp_address, :smtp_port, :smtp_domain, :smtp_enable_starttls_auto, + EDITABLE_ATTRS = [:email, :imap_enabled, :imap_login, :imap_password, :imap_address, :imap_port, :imap_enable_ssl, :imap_inbox_synced_at, + :smtp_enabled, :smtp_login, :smtp_password, :smtp_address, :smtp_port, :smtp_domain, :smtp_enable_starttls_auto, :smtp_enable_ssl_tls, :smtp_openssl_verify_mode, :smtp_authentication].freeze validates :email, uniqueness: true diff --git a/app/views/api/v1/models/_inbox.json.jbuilder b/app/views/api/v1/models/_inbox.json.jbuilder index dded0ec0d..17bde23e0 100644 --- a/app/views/api/v1/models/_inbox.json.jbuilder +++ b/app/views/api/v1/models/_inbox.json.jbuilder @@ -53,7 +53,7 @@ if resource.email? json.email resource.channel.try(:email) ## IMAP - json.imap_email resource.channel.try(:imap_email) + json.imap_login resource.channel.try(:imap_login) json.imap_password resource.channel.try(:imap_password) json.imap_address resource.channel.try(:imap_address) json.imap_port resource.channel.try(:imap_port) @@ -61,7 +61,7 @@ if resource.email? json.imap_enable_ssl resource.channel.try(:imap_enable_ssl) ## SMTP - json.smtp_email resource.channel.try(:smtp_email) + json.smtp_login resource.channel.try(:smtp_login) json.smtp_password resource.channel.try(:smtp_password) json.smtp_address resource.channel.try(:smtp_address) json.smtp_port resource.channel.try(:smtp_port) diff --git a/config/schedule.yml b/config/schedule.yml index 156a19787..94aca4896 100644 --- a/config/schedule.yml +++ b/config/schedule.yml @@ -16,6 +16,6 @@ trigger_scheduled_items_job: # executed At every 5th minute.. trigger_imap_email_inboxes_job: - cron: '*/5 * * * *' + cron: '*/1 * * * *' class: 'Inboxes::FetchImapEmailInboxesJob' queue: scheduled_jobs diff --git a/db/migrate/20220409044943_rename_imap_email_and_smtp_email_columns.rb b/db/migrate/20220409044943_rename_imap_email_and_smtp_email_columns.rb new file mode 100644 index 000000000..4518647b4 --- /dev/null +++ b/db/migrate/20220409044943_rename_imap_email_and_smtp_email_columns.rb @@ -0,0 +1,6 @@ +class RenameImapEmailAndSmtpEmailColumns < ActiveRecord::Migration[6.1] + def change + rename_column :channel_email, :imap_email, :imap_login + rename_column :channel_email, :smtp_email, :smtp_login + end +end diff --git a/db/schema.rb b/db/schema.rb index 84619b696..93b96cec7 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: 2022_04_05_092033) do +ActiveRecord::Schema.define(version: 2022_04_09_044943) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" @@ -190,14 +190,14 @@ ActiveRecord::Schema.define(version: 2022_04_05_092033) do t.boolean "imap_enabled", default: false t.string "imap_address", default: "" t.integer "imap_port", default: 0 - t.string "imap_email", default: "" + t.string "imap_login", default: "" t.string "imap_password", default: "" t.boolean "imap_enable_ssl", default: true t.datetime "imap_inbox_synced_at" t.boolean "smtp_enabled", default: false t.string "smtp_address", default: "" t.integer "smtp_port", default: 0 - t.string "smtp_email", default: "" + t.string "smtp_login", default: "" t.string "smtp_password", default: "" t.string "smtp_domain", default: "" t.boolean "smtp_enable_starttls_auto", default: true diff --git a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb index 420a94835..6cf33e7ad 100644 --- a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb @@ -426,7 +426,7 @@ RSpec.describe 'Inboxes API', type: :request do imap_enabled: true, imap_address: 'imap.gmail.com', imap_port: 993, - imap_email: 'imaptest@gmail.com' + imap_login: 'imaptest@gmail.com' } }, as: :json @@ -496,7 +496,7 @@ RSpec.describe 'Inboxes API', type: :request do smtp_enabled: true, smtp_address: 'smtp.gmail.com', smtp_port: 587, - smtp_email: 'smtptest@gmail.com', + smtp_login: 'smtptest@gmail.com', smtp_enable_starttls_auto: true, smtp_openssl_verify_mode: 'peer' } @@ -525,7 +525,7 @@ RSpec.describe 'Inboxes API', type: :request do channel: { smtp_enabled: true, smtp_address: 'smtp.gmail.com', - smtp_email: 'smtptest@gmail.com', + smtp_login: 'smtptest@gmail.com', smtp_port: 587, smtp_enable_ssl_tls: true, smtp_openssl_verify_mode: 'none' diff --git a/spec/jobs/inboxes/fetch_imap_email_inboxes_job_spec.rb b/spec/jobs/inboxes/fetch_imap_email_inboxes_job_spec.rb index 210602ac9..0b23ef9c4 100644 --- a/spec/jobs/inboxes/fetch_imap_email_inboxes_job_spec.rb +++ b/spec/jobs/inboxes/fetch_imap_email_inboxes_job_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe Inboxes::FetchImapEmailInboxesJob, type: :job do let(:account) { create(:account) } let(:imap_email_channel) do - create(:channel_email, imap_enabled: true, imap_address: 'imap.gmail.com', imap_port: 993, imap_email: 'imap@gmail.com', + create(:channel_email, imap_enabled: true, imap_address: 'imap.gmail.com', imap_port: 993, imap_login: 'imap@gmail.com', imap_password: 'password', account: account) end let(:email_inbox) { create(:inbox, channel: imap_email_channel, account: account) } diff --git a/spec/jobs/inboxes/fetch_imap_emails_job_spec.rb b/spec/jobs/inboxes/fetch_imap_emails_job_spec.rb index 6c1beecaf..e61198bed 100644 --- a/spec/jobs/inboxes/fetch_imap_emails_job_spec.rb +++ b/spec/jobs/inboxes/fetch_imap_emails_job_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe Inboxes::FetchImapEmailsJob, type: :job do let(:account) { create(:account) } let(:imap_email_channel) do - create(:channel_email, imap_enabled: true, imap_address: 'imap.gmail.com', imap_port: 993, imap_email: 'imap@gmail.com', + create(:channel_email, imap_enabled: true, imap_address: 'imap.gmail.com', imap_port: 993, imap_login: 'imap@gmail.com', imap_password: 'password', imap_inbox_synced_at: Time.now.utc - 10, account: account) end let(:email_inbox) { create(:inbox, channel: imap_email_channel, account: account) } diff --git a/spec/mailboxes/imap/imap_mailbox_spec.rb b/spec/mailboxes/imap/imap_mailbox_spec.rb index 0f44e2a40..a78c4fcf1 100644 --- a/spec/mailboxes/imap/imap_mailbox_spec.rb +++ b/spec/mailboxes/imap/imap_mailbox_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Imap::ImapMailbox, type: :mailbox do let(:agent) { create(:user, email: 'agent@example.com', account: account) } let(:channel) do create(:channel_email, imap_enabled: true, imap_address: 'imap.gmail.com', - imap_port: 993, imap_email: 'imap@gmail.com', imap_password: 'password', + imap_port: 993, imap_login: 'imap@gmail.com', imap_password: 'password', account: account) end let(:inbox) { create(:inbox, channel: channel, account: account) } diff --git a/spec/mailers/conversation_reply_mailer_spec.rb b/spec/mailers/conversation_reply_mailer_spec.rb index c1d6d18dd..c64b2a28a 100644 --- a/spec/mailers/conversation_reply_mailer_spec.rb +++ b/spec/mailers/conversation_reply_mailer_spec.rb @@ -156,7 +156,7 @@ RSpec.describe ConversationReplyMailer, type: :mailer do context 'when smtp enabled for email channel' do let(:smtp_email_channel) do - create(:channel_email, smtp_enabled: true, smtp_address: 'smtp.gmail.com', smtp_port: 587, smtp_email: 'smtp@gmail.com', + create(:channel_email, smtp_enabled: true, smtp_address: 'smtp.gmail.com', smtp_port: 587, smtp_login: 'smtp@gmail.com', smtp_password: 'password', smtp_domain: 'smtp.gmail.com', account: account) end let(:conversation) { create(:conversation, assignee: agent, inbox: smtp_email_channel.inbox, account: account).reload } @@ -251,5 +251,24 @@ RSpec.describe ConversationReplyMailer, type: :mailer do expect(mail.in_reply_to).to eq("account/#{conversation.account.id}/conversation/#{conversation.uuid}@#{conversation.account.domain}") end end + + context 'when inbound email domain is not enabled' do + let(:new_account) { create(:account, domain: nil) } + let!(:email_channel) { create(:channel_email, account: new_account) } + let!(:inbox) { create(:inbox, channel: email_channel, account: new_account) } + let(:inbox_member) { create(:inbox_member, user: agent, inbox: inbox) } + let(:conversation) { create(:conversation, assignee: agent, inbox: inbox_member.inbox, account: new_account) } + let!(:message) { create(:message, conversation: conversation, account: new_account) } + let(:mail) { described_class.reply_with_summary(message.conversation, message.id).deliver_now } + let(:domain) { inbox.channel.email.split('@').last } + + it 'sets the correct custom message id' do + expect(mail.message_id).to eq("conversation/#{conversation.uuid}/messages/#{message.id}@#{domain}") + end + + it 'sets the correct in reply to id' do + expect(mail.in_reply_to).to eq("account/#{conversation.account.id}/conversation/#{conversation.uuid}@#{domain}") + end + end end end From c64e2e3bc5e4b6579366e8ae9b0fca19ed3932ae Mon Sep 17 00:00:00 2001 From: "Aswin Dev P.S" Date: Mon, 11 Apr 2022 20:57:22 +0530 Subject: [PATCH 043/177] chore: Report improvements (#4392) Co-authored-by: Pranav Raj S --- .../dashboard/i18n/locale/en/report.json | 32 +++++++++----- .../mixins/specs/reportMixin.spec.js | 2 +- .../dashboard/settings/reports/Index.vue | 24 +++++++++- .../reports/components/ReportFilters.vue | 6 +-- .../reports/components/WootReports.vue | 23 +++++++++- .../dashboard/settings/reports/constants.js | 44 +++++-------------- .../dashboard/store/modules/reports.js | 10 ----- package.json | 2 +- yarn.lock | 8 ++-- 9 files changed, 86 insertions(+), 65 deletions(-) diff --git a/app/javascript/dashboard/i18n/locale/en/report.json b/app/javascript/dashboard/i18n/locale/en/report.json index a14704007..1574b1cca 100644 --- a/app/javascript/dashboard/i18n/locale/en/report.json +++ b/app/javascript/dashboard/i18n/locale/en/report.json @@ -20,12 +20,14 @@ "FIRST_RESPONSE_TIME": { "NAME": "First Response Time", "DESC": "( Avg )", - "INFO_TEXT": "Total number of conversations used for computation:" + "INFO_TEXT": "Total number of conversations used for computation:", + "TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)" }, "RESOLUTION_TIME": { "NAME": "Resolution Time", "DESC": "( Avg )", - "INFO_TEXT": "Total number of conversations used for computation:" + "INFO_TEXT": "Total number of conversations used for computation:", + "TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)" }, "RESOLUTION_COUNT": { "NAME": "Resolution Count", @@ -103,12 +105,14 @@ "FIRST_RESPONSE_TIME": { "NAME": "First Response Time", "DESC": "( Avg )", - "INFO_TEXT": "Total number of conversations used for computation:" + "INFO_TEXT": "Total number of conversations used for computation:", + "TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)" }, "RESOLUTION_TIME": { "NAME": "Resolution Time", "DESC": "( Avg )", - "INFO_TEXT": "Total number of conversations used for computation:" + "INFO_TEXT": "Total number of conversations used for computation:", + "TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)" }, "RESOLUTION_COUNT": { "NAME": "Resolution Count", @@ -168,12 +172,14 @@ "FIRST_RESPONSE_TIME": { "NAME": "First Response Time", "DESC": "( Avg )", - "INFO_TEXT": "Total number of conversations used for computation:" + "INFO_TEXT": "Total number of conversations used for computation:", + "TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)" }, "RESOLUTION_TIME": { "NAME": "Resolution Time", "DESC": "( Avg )", - "INFO_TEXT": "Total number of conversations used for computation:" + "INFO_TEXT": "Total number of conversations used for computation:", + "TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)" }, "RESOLUTION_COUNT": { "NAME": "Resolution Count", @@ -233,12 +239,14 @@ "FIRST_RESPONSE_TIME": { "NAME": "First Response Time", "DESC": "( Avg )", - "INFO_TEXT": "Total number of conversations used for computation:" + "INFO_TEXT": "Total number of conversations used for computation:", + "TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)" }, "RESOLUTION_TIME": { "NAME": "Resolution Time", "DESC": "( Avg )", - "INFO_TEXT": "Total number of conversations used for computation:" + "INFO_TEXT": "Total number of conversations used for computation:", + "TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)" }, "RESOLUTION_COUNT": { "NAME": "Resolution Count", @@ -298,12 +306,14 @@ "FIRST_RESPONSE_TIME": { "NAME": "First Response Time", "DESC": "( Avg )", - "INFO_TEXT": "Total number of conversations used for computation:" + "INFO_TEXT": "Total number of conversations used for computation:", + "TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)" }, "RESOLUTION_TIME": { "NAME": "Resolution Time", "DESC": "( Avg )", - "INFO_TEXT": "Total number of conversations used for computation:" + "INFO_TEXT": "Total number of conversations used for computation:", + "TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)" }, "RESOLUTION_COUNT": { "NAME": "Resolution Count", @@ -372,4 +382,4 @@ } } } -} \ No newline at end of file +} diff --git a/app/javascript/dashboard/mixins/specs/reportMixin.spec.js b/app/javascript/dashboard/mixins/specs/reportMixin.spec.js index ca1c45751..aa3c451d9 100644 --- a/app/javascript/dashboard/mixins/specs/reportMixin.spec.js +++ b/app/javascript/dashboard/mixins/specs/reportMixin.spec.js @@ -25,7 +25,7 @@ describe('reportMixin', () => { const wrapper = shallowMount(Component, { store, localVue }); expect(wrapper.vm.displayMetric('conversations_count')).toEqual(5); expect(wrapper.vm.displayMetric('avg_first_response_time')).toEqual( - '3 Min' + '3 Min 18 Sec' ); }); diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue index f9a9256f4..bc6ac48de 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue @@ -57,6 +57,7 @@ import format from 'date-fns/format'; import ReportFilterSelector from './components/FilterSelector'; import { GROUP_BY_FILTER, METRIC_CHART } from './constants'; import reportMixin from '../../../../mixins/reportMixin'; +import { formatTime } from '@chatwoot/utils'; const REPORTS_KEYS = { CONVERSATIONS: 'conversations_count', @@ -145,8 +146,22 @@ export default { }; }, chartOptions() { + let tooltips = {}; + if (this.isAverageMetricType(this.metrics[this.currentSelection].KEY)) { + tooltips.callbacks = { + label: tooltipItem => { + return this.$t(this.metrics[this.currentSelection].TOOLTIP_TEXT, { + metricValue: formatTime(tooltipItem.yLabel), + conversationCount: this.accountReport.data[tooltipItem.index] + .count, + }); + }, + }; + } + return { scales: METRIC_CHART[this.metrics[this.currentSelection].KEY].scales, + tooltips: tooltips, }; }, metrics() { @@ -158,11 +173,18 @@ export default { 'RESOLUTION_TIME', 'RESOLUTION_COUNT', ]; + const infoText = { + FIRST_RESPONSE_TIME: this.$t( + `REPORT.METRICS.FIRST_RESPONSE_TIME.INFO_TEXT` + ), + RESOLUTION_TIME: this.$t(`REPORT.METRICS.RESOLUTION_TIME.INFO_TEXT`), + }; return reportKeys.map(key => ({ NAME: this.$t(`REPORT.METRICS.${key}.NAME`), KEY: REPORTS_KEYS[key], DESC: this.$t(`REPORT.METRICS.${key}.DESC`), - INFO_TEXT: this.$t(`REPORT.METRICS.${key}.INFO_TEXT`), + INFO_TEXT: infoText[key], + TOOLTIP_TEXT: `REPORT.METRICS.${key}.TOOLTIP_TEXT`, })); }, }, diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue index 0bfeae8a1..c0b87e19b 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue @@ -148,9 +148,9 @@
{{ $t('REPORT.BUSINESS_HOURS') }} - - - + + +
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue index 7a54765b8..6d8b49c62 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue @@ -60,6 +60,7 @@ import fromUnixTime from 'date-fns/fromUnixTime'; import format from 'date-fns/format'; import { GROUP_BY_FILTER, METRIC_CHART } from '../constants'; import reportMixin from '../../../../../mixins/reportMixin'; +import { formatTime } from '@chatwoot/utils'; const REPORTS_KEYS = { CONVERSATIONS: 'conversations_count', @@ -171,8 +172,21 @@ export default { }; }, chartOptions() { + let tooltips = {}; + if (this.isAverageMetricType(this.metrics[this.currentSelection].KEY)) { + tooltips.callbacks = { + label: tooltipItem => { + return this.$t(this.metrics[this.currentSelection].TOOLTIP_TEXT, { + metricValue: formatTime(tooltipItem.yLabel), + conversationCount: this.accountReport.data[tooltipItem.index] + .count, + }); + }, + }; + } return { scales: METRIC_CHART[this.metrics[this.currentSelection].KEY].scales, + tooltips: tooltips, }; }, metrics() { @@ -190,11 +204,18 @@ export default { 'RESOLUTION_TIME', 'RESOLUTION_COUNT', ]; + const infoText = { + FIRST_RESPONSE_TIME: this.$t( + `REPORT.METRICS.FIRST_RESPONSE_TIME.INFO_TEXT` + ), + RESOLUTION_TIME: this.$t(`REPORT.METRICS.RESOLUTION_TIME.INFO_TEXT`), + }; return reportKeys.map(key => ({ NAME: this.$t(`REPORT.METRICS.${key}.NAME`), KEY: REPORTS_KEYS[key], DESC: this.$t(`REPORT.METRICS.${key}.DESC`), - INFO_TEXT: this.$t(`REPORT.METRICS.${key}.INFO_TEXT`), + INFO_TEXT: infoText[key], + TOOLTIP_TEXT: `REPORT.METRICS.${key}.TOOLTIP_TEXT`, })); }, }, diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/constants.js b/app/javascript/dashboard/routes/dashboard/settings/reports/constants.js index 91348543e..c44f2bdef 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/constants.js +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/constants.js @@ -1,3 +1,5 @@ +import { formatTime } from '@chatwoot/utils'; + export const GROUP_BY_FILTER = { 1: { id: 1, period: 'day' }, 2: { id: 2, period: 'week' }, @@ -17,7 +19,7 @@ export const DEFAULT_LINE_CHART = { export const DEFAULT_BAR_CHART = { type: 'bar', - backgroundColor: 'rgb(31, 147, 255, 0.5)', + backgroundColor: 'rgb(31, 147, 255)', }; export const DEFAULT_CHART = { @@ -56,7 +58,7 @@ export const METRIC_CHART = { incoming_messages_count: DEFAULT_CHART, outgoing_messages_count: DEFAULT_CHART, avg_first_response_time: { - datasets: [DEFAULT_BAR_CHART, DEFAULT_LINE_CHART], + datasets: [DEFAULT_BAR_CHART], scales: { xAxes: [ { @@ -75,21 +77,9 @@ export const METRIC_CHART = { position: 'left', ticks: { fontFamily: CHART_FONT_FAMILY, - beginAtZero: true, - precision: 2, - }, - gridLines: { - drawOnChartArea: false, - }, - }, - { - id: 'y-right', - type: 'linear', - position: 'right', - ticks: { - fontFamily: CHART_FONT_FAMILY, - beginAtZero: true, - stepSize: 1, + callback(value) { + return formatTime(value); + }, }, gridLines: { drawOnChartArea: false, @@ -99,7 +89,7 @@ export const METRIC_CHART = { }, }, avg_resolution_time: { - datasets: [DEFAULT_BAR_CHART, DEFAULT_LINE_CHART], + datasets: [DEFAULT_BAR_CHART], scales: { xAxes: [ { @@ -118,21 +108,9 @@ export const METRIC_CHART = { position: 'left', ticks: { fontFamily: CHART_FONT_FAMILY, - beginAtZero: true, - precision: 2, - }, - gridLines: { - drawOnChartArea: false, - }, - }, - { - id: 'y-right', - type: 'linear', - position: 'right', - ticks: { - fontFamily: CHART_FONT_FAMILY, - beginAtZero: true, - stepSize: 1, + callback(value) { + return formatTime(value); + }, }, gridLines: { drawOnChartArea: false, diff --git a/app/javascript/dashboard/store/modules/reports.js b/app/javascript/dashboard/store/modules/reports.js index 67f468add..0b31883bf 100644 --- a/app/javascript/dashboard/store/modules/reports.js +++ b/app/javascript/dashboard/store/modules/reports.js @@ -50,16 +50,6 @@ export const actions = { el => reportObj.to - el.timestamp > 0 && el.timestamp - reportObj.from >= 0 ); - if ( - reportObj.metric === 'avg_first_response_time' || - reportObj.metric === 'avg_resolution_time' - ) { - data = data.map(element => { - /* eslint-disable operator-assignment */ - element.value = (element.value / 3600).toFixed(2); - return element; - }); - } commit(types.default.SET_ACCOUNT_REPORTS, data); commit(types.default.TOGGLE_ACCOUNT_REPORT_LOADING, false); }); diff --git a/package.json b/package.json index 7371322b6..1b55668b3 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "dependencies": { "@braid/vue-formulate": "^2.5.2", "@chatwoot/prosemirror-schema": "https://github.com/chatwoot/prosemirror-schema.git#7e8acadd10d7b932c0dc0bd0a18f804434f83517", - "@chatwoot/utils": "^0.0.5", + "@chatwoot/utils": "^0.0.6", "@hcaptcha/vue-hcaptcha": "^0.3.2", "@rails/actioncable": "6.1.3", "@rails/webpacker": "5.3.0", diff --git a/yarn.lock b/yarn.lock index b4c4e505c..a95d08798 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1111,10 +1111,10 @@ prosemirror-state "^1.3.3" prosemirror-view "^1.17.2" -"@chatwoot/utils@^0.0.5": - version "0.0.5" - resolved "https://registry.npmjs.org/@chatwoot/utils/-/utils-0.0.5.tgz#907cdae747abc17cf2e5c31a378aba66a2f31c6f" - integrity sha512-gTQMpQuYVF5EaF4+xSmaoJKXPtbwDPjNLi5cwg44FaQSfmzFxD0fjDsDUPrxzZfURLsR8eU7Z1ulVKnZdHN4yg== +"@chatwoot/utils@^0.0.6": + version "0.0.6" + resolved "https://registry.yarnpkg.com/@chatwoot/utils/-/utils-0.0.6.tgz#76d7b17d692b5b656c565b9b714b98e0f2bc1324" + integrity sha512-fCvULfJSFSylDAiGh1cPAX5nQkVsmG5ASGm/E6YBYg8cox/2JU179JFstdtTxrIJg/YeHukcaq85Gc+/16ShPQ== dependencies: date-fns "^2.22.1" From 14503b5fe0940d90219a27d5c3ba1a04b3de9588 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Tue, 12 Apr 2022 11:08:12 +0530 Subject: [PATCH 044/177] feat: Add missing password validation at signup (#4441) Co-authored-by: Pranav Raj S --- .../dashboard/i18n/locale/en/signup.json | 3 ++- .../dashboard/routes/auth/Signup.vue | 22 ++++++++++++++----- app/javascript/shared/helpers/Validators.js | 14 ++++++++++++ .../helpers/specs/ValidatorsHelper.spec.js | 17 ++++++++++++++ 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/app/javascript/dashboard/i18n/locale/en/signup.json b/app/javascript/dashboard/i18n/locale/en/signup.json index 6eaa5d646..8dd5c0d4e 100644 --- a/app/javascript/dashboard/i18n/locale/en/signup.json +++ b/app/javascript/dashboard/i18n/locale/en/signup.json @@ -21,7 +21,8 @@ "PASSWORD": { "LABEL": "Password", "PLACEHOLDER": "Password", - "ERROR": "Password is too short" + "ERROR": "Password is too short", + "IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character" }, "CONFIRM_PASSWORD": { "LABEL": "Confirm Password", diff --git a/app/javascript/dashboard/routes/auth/Signup.vue b/app/javascript/dashboard/routes/auth/Signup.vue index 4f5aab8bd..2ea4e443a 100644 --- a/app/javascript/dashboard/routes/auth/Signup.vue +++ b/app/javascript/dashboard/routes/auth/Signup.vue @@ -54,14 +54,9 @@ :class="{ error: $v.credentials.password.$error }" :label="$t('LOGIN.PASSWORD.LABEL')" :placeholder="$t('SET_NEW_PASSWORD.PASSWORD.PLACEHOLDER')" - :error=" - $v.credentials.password.$error - ? $t('SET_NEW_PASSWORD.PASSWORD.ERROR') - : '' - " + :error="passwordErrorText" @blur="$v.credentials.password.$touch" /> - !!value.match(/^\+[1-9]\d{1,14}$/); export const isPhoneE164OrEmpty = value => isPhoneE164(value) || value === ''; export const shouldBeUrl = (value = '') => value ? value.startsWith('http') : true; +export const isValidPassword = value => { + const containsUppercase = /[A-Z]/.test(value); + const containsLowercase = /[a-z]/.test(value); + const containsNumber = /[0-9]/.test(value); + const containsSpecialCharacter = /[!@#$%^&*()_+\-=[\]{}|'"/\\.,`<>:;?~]/.test( + value + ); + return ( + containsUppercase && + containsLowercase && + containsNumber && + containsSpecialCharacter + ); +}; diff --git a/app/javascript/shared/helpers/specs/ValidatorsHelper.spec.js b/app/javascript/shared/helpers/specs/ValidatorsHelper.spec.js index ba393a7a3..0abc12c84 100644 --- a/app/javascript/shared/helpers/specs/ValidatorsHelper.spec.js +++ b/app/javascript/shared/helpers/specs/ValidatorsHelper.spec.js @@ -1,7 +1,24 @@ import { shouldBeUrl } from '../Validators'; +import { isValidPassword } from '../Validators'; describe('#shouldBeUrl', () => { it('should return correct url', () => { expect(shouldBeUrl('http')).toEqual(true); }); }); + +describe('#isValidPassword', () => { + it('should return correct password', () => { + expect(isValidPassword('testPass4!')).toEqual(true); + expect(isValidPassword('testPass4-')).toEqual(true); + expect(isValidPassword('testPass4\\')).toEqual(true); + expect(isValidPassword("testPass4'")).toEqual(true); + }); + + it('should return wrong password', () => { + expect(isValidPassword('testpass4')).toEqual(false); + expect(isValidPassword('testPass4')).toEqual(false); + expect(isValidPassword('testpass4!')).toEqual(false); + expect(isValidPassword('testPass!')).toEqual(false); + }); +}); From b6ad468eb442ed36b92e892f0bc00a5176492dbd Mon Sep 17 00:00:00 2001 From: Tejaswini Chile Date: Tue, 12 Apr 2022 11:30:54 +0530 Subject: [PATCH 045/177] fix: Nokogiri bundle update (#4448) Co-authored-by: Muhsin Keloth --- Gemfile.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 4b128f424..2789f09da 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -378,14 +378,14 @@ GEM netrc (0.11.0) newrelic_rpm (8.4.0) nio4r (2.5.8) - nokogiri (1.13.3) + nokogiri (1.13.4) mini_portile2 (~> 2.8.0) racc (~> 1.4) - nokogiri (1.13.3-arm64-darwin) + nokogiri (1.13.4-arm64-darwin) racc (~> 1.4) - nokogiri (1.13.3-x86_64-darwin) + nokogiri (1.13.4-x86_64-darwin) racc (~> 1.4) - nokogiri (1.13.3-x86_64-linux) + nokogiri (1.13.4-x86_64-linux) racc (~> 1.4) oauth (0.5.8) orm_adapter (0.5.0) From 923b4637db3d255da9607b0b60206c23d4cee02c Mon Sep 17 00:00:00 2001 From: Tejaswini Chile Date: Tue, 12 Apr 2022 20:23:34 +0530 Subject: [PATCH 046/177] chore: Automation bug fix (#4442) --- .../settings/automation/AddAutomationRule.vue | 2 +- .../automation/EditAutomationRule.vue | 2 +- .../settings/automation/constants.js | 8 +- app/listeners/automation_rule_listener.rb | 22 ++-- .../automation_notification_mailer.rb | 12 +- app/models/conversation.rb | 5 +- app/models/message.rb | 10 +- .../automation_rules/action_service.rb | 2 +- .../conversation_creation.liquid | 3 +- .../automation_rules_controller_spec.rb | 16 ++- .../automation_rule_listener_spec.rb | 119 ------------------ spec/models/conversation_spec.rb | 10 +- 12 files changed, 47 insertions(+), 164 deletions(-) diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/AddAutomationRule.vue b/app/javascript/dashboard/routes/dashboard/settings/automation/AddAutomationRule.vue index 68d2c687c..73a02fd1a 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/automation/AddAutomationRule.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/automation/AddAutomationRule.vue @@ -194,7 +194,7 @@ export default { required: requiredIf(prop => { return !( prop.action_name === 'mute_conversation' || - prop.action_name === 'snooze_convresation' || + prop.action_name === 'snooze_conversation' || prop.action_name === 'resolve_convresation' ); }), diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/EditAutomationRule.vue b/app/javascript/dashboard/routes/dashboard/settings/automation/EditAutomationRule.vue index cf70a35bb..14a8e84bb 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/automation/EditAutomationRule.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/automation/EditAutomationRule.vue @@ -198,7 +198,7 @@ export default { required: requiredIf(prop => { return !( prop.action_name === 'mute_conversation' || - prop.action_name === 'snooze_convresation' || + prop.action_name === 'snooze_conversation' || prop.action_name === 'resolve_convresation' ); }), diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js b/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js index dd11700bd..cd306007a 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js +++ b/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js @@ -92,7 +92,7 @@ export const AUTOMATIONS = { attributeI18nKey: 'MUTE_CONVERSATION', }, { - key: 'snooze_convresation', + key: 'snooze_conversation', name: 'Snooze conversation', attributeI18nKey: 'MUTE_CONVERSATION', }, @@ -166,7 +166,7 @@ export const AUTOMATIONS = { attributeI18nKey: 'MUTE_CONVERSATION', }, { - key: 'snooze_convresation', + key: 'snooze_conversation', name: 'Snooze conversation', attributeI18nKey: 'MUTE_CONVERSATION', }, @@ -254,7 +254,7 @@ export const AUTOMATIONS = { attributeI18nKey: 'MUTE_CONVERSATION', }, { - key: 'snooze_convresation', + key: 'snooze_conversation', name: 'Snooze conversation', attributeI18nKey: 'MUTE_CONVERSATION', }, @@ -314,7 +314,7 @@ export const AUTOMATION_ACTION_TYPES = [ inputType: null, }, { - key: 'snooze_convresation', + key: 'snooze_conversation', label: 'Snooze conversation', inputType: null, }, diff --git a/app/listeners/automation_rule_listener.rb b/app/listeners/automation_rule_listener.rb index 19fb56287..c8c021a4e 100644 --- a/app/listeners/automation_rule_listener.rb +++ b/app/listeners/automation_rule_listener.rb @@ -1,5 +1,7 @@ class AutomationRuleListener < BaseListener def conversation_updated(event_obj) + return if performed_by_automation?(event_obj) + conversation = event_obj.data[:conversation] account = conversation.account @@ -11,19 +13,9 @@ class AutomationRuleListener < BaseListener end end - def conversation_status_changed(event_obj) - conversation = event_obj.data[:conversation] - account = conversation.account - - return unless rule_present?('conversation_status_changed', account) - - @rules.each do |rule| - conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, conversation).perform - AutomationRules::ActionService.new(rule, account, conversation).perform if conditions_match.present? - end - end - def conversation_created(event_obj) + return if performed_by_automation?(event_obj) + conversation = event_obj.data[:conversation] account = conversation.account @@ -36,6 +28,8 @@ class AutomationRuleListener < BaseListener end def message_created(event_obj) + return if performed_by_automation?(event_obj) + message = event_obj.data[:message] account = message.try(:account) @@ -57,4 +51,8 @@ class AutomationRuleListener < BaseListener ) @rules.any? end + + def performed_by_automation?(event_obj) + event_obj.data[:performed_by].present? && event_obj.data[:performed_by].instance_of?(AutomationRule) + end end diff --git a/app/mailers/team_notifications/automation_notification_mailer.rb b/app/mailers/team_notifications/automation_notification_mailer.rb index fd96cdd1d..d6ca47f81 100644 --- a/app/mailers/team_notifications/automation_notification_mailer.rb +++ b/app/mailers/team_notifications/automation_notification_mailer.rb @@ -35,18 +35,14 @@ class TeamNotifications::AutomationNotificationMailer < ApplicationMailer private def send_an_email_to_team - @agents.each do |agent| - subject = "#{agent.user.available_name}, This email has been sent via automation rule actions." - @action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id) - @agent = agent - - send_mail_with_liquid(to: @agent.user.email, subject: subject) - end + subject = 'This email has been sent via automation rule actions.' + @action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id) + @agent_emails = @agents.collect(&:user).pluck(:email) + send_mail_with_liquid(to: @agent_emails, subject: subject) and return end def liquid_droppables super.merge!({ - user: @agent.user, conversation: @conversation, inbox: @conversation.inbox }) diff --git a/app/models/conversation.rb b/app/models/conversation.rb index 75433f7fe..409efab6d 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -214,10 +214,9 @@ class Conversation < ApplicationRecord end def dispatcher_dispatch(event_name, changed_attributes = nil) - return if Current.executed_by.present? && Current.executed_by.instance_of?(AutomationRule) - Rails.configuration.dispatcher.dispatch(event_name, Time.zone.now, conversation: self, notifiable_assignee_change: notifiable_assignee_change?, - changed_attributes: changed_attributes) + changed_attributes: changed_attributes, + performed_by: Current.executed_by) end def conversation_status_changed_to_open? diff --git a/app/models/message.rb b/app/models/message.rb index adfc914b6..13d58db6b 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -166,19 +166,15 @@ class Message < ApplicationRecord end def dispatch_create_events - return if Current.executed_by.present? && Current.executed_by.instance_of?(AutomationRule) - - Rails.configuration.dispatcher.dispatch(MESSAGE_CREATED, Time.zone.now, message: self) + Rails.configuration.dispatcher.dispatch(MESSAGE_CREATED, Time.zone.now, message: self, performed_by: Current.executed_by) if outgoing? && conversation.messages.outgoing.count == 1 - Rails.configuration.dispatcher.dispatch(FIRST_REPLY_CREATED, Time.zone.now, message: self) + Rails.configuration.dispatcher.dispatch(FIRST_REPLY_CREATED, Time.zone.now, message: self, performed_by: Current.executed_by) end end def dispatch_update_event - return if Current.executed_by.present? && Current.executed_by.instance_of?(AutomationRule) - - Rails.configuration.dispatcher.dispatch(MESSAGE_UPDATED, Time.zone.now, message: self) + Rails.configuration.dispatcher.dispatch(MESSAGE_UPDATED, Time.zone.now, message: self, performed_by: Current.executed_by) end def send_reply diff --git a/app/services/automation_rules/action_service.rb b/app/services/automation_rules/action_service.rb index 75b1f0474..1249bd9c5 100644 --- a/app/services/automation_rules/action_service.rb +++ b/app/services/automation_rules/action_service.rb @@ -41,7 +41,7 @@ class AutomationRules::ActionService end def snooze_conversation(_params) - @conversation.ensure_snooze_until_reset + @conversation.snoozed! end def change_status(status) 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 index 8edefcc07..036e96603 100644 --- a/app/views/mailers/team_notifications/automation_notification_mailer/conversation_creation.liquid +++ b/app/views/mailers/team_notifications/automation_notification_mailer/conversation_creation.liquid @@ -1,5 +1,4 @@ -

Hi {{user.available_name}}

- +

This is the mail from Automation System

{{ custom_message }}

diff --git a/spec/controllers/api/v1/accounts/automation_rules_controller_spec.rb b/spec/controllers/api/v1/accounts/automation_rules_controller_spec.rb index 0d3b3e784..0b921bc07 100644 --- a/spec/controllers/api/v1/accounts/automation_rules_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/automation_rules_controller_spec.rb @@ -259,7 +259,7 @@ RSpec.describe 'Api::V1::Accounts::AutomationRulesController', type: :request do end context 'when it is an authenticated user' do - it 'returns for cloned automation_rule for account' do + it 'returns for updated automation_rule for account' do params = { name: 'Update name' } expect(account.automation_rules.count).to eq(1) @@ -271,6 +271,20 @@ RSpec.describe 'Api::V1::Accounts::AutomationRulesController', type: :request do body = JSON.parse(response.body, symbolize_names: true) expect(body[:payload][:name]).to eq('Update name') end + + it 'returns for updated active flag for automation_rule' do + expect(automation_rule.active).to eq(true) + params = { active: false } + + patch "/api/v1/accounts/#{account.id}/automation_rules/#{automation_rule.id}", + headers: administrator.create_new_auth_token, + params: params + + expect(response).to have_http_status(:success) + body = JSON.parse(response.body, symbolize_names: true) + expect(body[:payload][:active]).to eq(false) + expect(automation_rule.reload.active).to eq(false) + end end end diff --git a/spec/listeners/automation_rule_listener_spec.rb b/spec/listeners/automation_rule_listener_spec.rb index 6c4fc0c2a..b0ed4d0bd 100644 --- a/spec/listeners/automation_rule_listener_spec.rb +++ b/spec/listeners/automation_rule_listener_spec.rb @@ -50,125 +50,6 @@ describe AutomationRuleListener do automation_rule.save end - describe '#conversation_status_changed' do - context 'when rule matches' do - it 'triggers automation rule send webhook events' do - payload = conversation.webhook_data.merge(event: "automation_event: #{automation_rule.event_name}") - - automation_rule - - expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) - - expect(WebhookJob).to receive(:perform_later).with('https://www.example.com', payload).once - - listener.conversation_status_changed(event) - end - - it 'triggers automation rule to assign team' do - expect(conversation.team_id).not_to eq(team.id) - - automation_rule - - expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) - - 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 - - expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) - - listener.conversation_status_changed(event) - - conversation.reload - - expect(conversation.labels.pluck(:name)).to contain_exactly('support', 'priority_customer') - end - - it 'triggers automation rule to assign best agents' do - expect(conversation.assignee).to be_nil - - automation_rule - - expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) - - listener.conversation_status_changed(event) - - conversation.reload - - expect(conversation.assignee).to eq(user_1) - end - - it 'triggers automation rule send message to the contacts' do - expect(conversation.messages).to be_empty - - automation_rule - - expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) - - listener.conversation_status_changed(event) - - conversation.reload - - expect(conversation.messages.first.content).to eq('Send this message.') - end - - it 'triggers automation rule changes status to snoozed' do - expect(conversation.status).to eq('resolved') - - automation_rule - - expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) - - listener.conversation_status_changed(event) - - conversation.reload - - expect(conversation.status).to eq('snoozed') - end - - it 'triggers automation rule send email transcript to the mentioned email' do - mailer = double - - automation_rule - - expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) - - listener.conversation_status_changed(event) - - conversation.reload - - allow(mailer).to receive(:conversation_transcript) - end - - it 'triggers automation rule send email to the team' do - automation_rule - - expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) - - listener.conversation_status_changed(event) - end - - it 'triggers automation rule send attachments in messages' do - automation_rule - - expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) - - listener.conversation_status_changed(event) - - conversation.reload - - expect(conversation.messages.last.attachments.count).to eq(1) - end - end - end - describe '#conversation_updated with contacts attributes' do before do conversation.contact.update!(custom_attributes: { customer_type: 'platinum', signed_in_at: '2022-01-19' }, diff --git a/spec/models/conversation_spec.rb b/spec/models/conversation_spec.rb index 6a9002795..d5295e340 100644 --- a/spec/models/conversation_spec.rb +++ b/spec/models/conversation_spec.rb @@ -55,7 +55,7 @@ RSpec.describe Conversation, type: :model do # send_events expect(Rails.configuration.dispatcher).to have_received(:dispatch) .with(described_class::CONVERSATION_CREATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: false, - changed_attributes: nil) + changed_attributes: nil, performed_by: nil) end end @@ -121,16 +121,16 @@ RSpec.describe Conversation, type: :model do expect(Rails.configuration.dispatcher).to have_received(:dispatch) .with(described_class::CONVERSATION_RESOLVED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true, - changed_attributes: status_change) + changed_attributes: status_change, performed_by: nil) expect(Rails.configuration.dispatcher).to have_received(:dispatch) .with(described_class::CONVERSATION_READ, kind_of(Time), conversation: conversation, notifiable_assignee_change: true, - changed_attributes: nil) + changed_attributes: nil, performed_by: nil) expect(Rails.configuration.dispatcher).to have_received(:dispatch) .with(described_class::ASSIGNEE_CHANGED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true, - changed_attributes: nil) + changed_attributes: nil, performed_by: nil) expect(Rails.configuration.dispatcher).to have_received(:dispatch) .with(described_class::CONVERSATION_UPDATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true, - changed_attributes: changed_attributes) + changed_attributes: changed_attributes, performed_by: nil) end it 'will not run conversation_updated event for empty updates' do From a6b119d1876b09a54675c04371c9d0a7323fccad Mon Sep 17 00:00:00 2001 From: Tejaswini Chile Date: Wed, 13 Apr 2022 01:29:51 +0530 Subject: [PATCH 047/177] Feat: twitter image support (#4429) --- .../twitter/direct_message_parser_service.rb | 52 ++++++++++++- .../twitter/webhook_subscribe_service.rb | 2 +- .../twitter/twitter_dm_image_event.rb | 75 +++++++++++++++++++ spec/lib/webhooks/twitter_spec.rb | 16 ++++ 4 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 spec/factories/twitter/twitter_dm_image_event.rb diff --git a/app/services/twitter/direct_message_parser_service.rb b/app/services/twitter/direct_message_parser_service.rb index 04e68e590..e3679f278 100644 --- a/app/services/twitter/direct_message_parser_service.rb +++ b/app/services/twitter/direct_message_parser_service.rb @@ -7,7 +7,7 @@ class Twitter::DirectMessageParserService < Twitter::WebhooksBaseService set_inbox ensure_contacts set_conversation - @conversation.messages.create( + @message = @conversation.messages.create( content: message_create_data['message_data']['text'], account_id: @inbox.account_id, inbox_id: @inbox.id, @@ -15,10 +15,24 @@ class Twitter::DirectMessageParserService < Twitter::WebhooksBaseService sender: @contact, source_id: direct_message_data['id'] ) + attach_files end private + def attach_files + return if message_create_data['message_data']['attachment'].blank? + + save_media + @message + end + + def save_media_urls(file) + @message.content_attributes[:media_url] = file['media_url'] + @message.content_attributes[:display_url] = file['display_url'] + @message.save + end + def direct_message_events_params payload['direct_message_events'] end @@ -39,6 +53,10 @@ class Twitter::DirectMessageParserService < Twitter::WebhooksBaseService ENV.fetch('TWITTER_APP_ID', '') end + def media + message_create_data['message_data']['attachment']['media'] + end + def users payload[:users] end @@ -73,4 +91,36 @@ class Twitter::DirectMessageParserService < Twitter::WebhooksBaseService def outgoing_message? message_create_data['sender_id'] == @inbox.channel.profile_id end + + def api_client + @api_client ||= begin + consumer = OAuth::Consumer.new(ENV.fetch('TWITTER_CONSUMER_KEY', nil), ENV.fetch('TWITTER_CONSUMER_SECRET', nil), + { site: 'https://api.twitter.com' }) + token = { oauth_token: @inbox.channel.twitter_access_token, oauth_token_secret: @inbox.channel.twitter_access_token_secret } + OAuth::AccessToken.from_hash(consumer, token) + end + end + + def save_media + save_media_urls(media) + response = api_client.get(media['media_url'], []) + + temp_file = Tempfile.new('twitter_attachment') + temp_file.binmode + temp_file << response.body + temp_file.rewind + + return unless media['type'] == 'photo' + + @message.attachments.new( + account_id: @inbox.account_id, + file_type: 'image', + file: { + io: temp_file, + filename: 'twitter_attachment', + content_type: media['type'] + } + ) + @message.save + end end diff --git a/app/services/twitter/webhook_subscribe_service.rb b/app/services/twitter/webhook_subscribe_service.rb index 7d7734231..f052bf551 100644 --- a/app/services/twitter/webhook_subscribe_service.rb +++ b/app/services/twitter/webhook_subscribe_service.rb @@ -43,7 +43,7 @@ class Twitter::WebhookSubscribeService def register_webhook register_response = twitter_client.register_webhook(url: twitter_url) - Rails.logger.info "TWITTER_UNREGISTER_WEBHOOK: #{register_response.body}" + Rails.logger.info "TWITTER_REGISTER_WEBHOOK: #{register_response.body}" end def subscription? diff --git a/spec/factories/twitter/twitter_dm_image_event.rb b/spec/factories/twitter/twitter_dm_image_event.rb new file mode 100644 index 000000000..dded4fc2d --- /dev/null +++ b/spec/factories/twitter/twitter_dm_image_event.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :twitter_dm_image_event, class: Hash do + for_user_id { '1' } + direct_message_events do + [{ + 'type' => 'message_create', + 'id' => '123', + 'message_create' => { + 'target' => { 'recipient_id' => '1' }, + 'sender_id' => '2', + 'source_app_id' => '268278', + 'message_data' => { + 'text' => 'Blue Bird', + 'attachment' => { + 'media' => { + 'display_url' => 'pic.twitter.com/5J1WJSRCy9', + 'expanded_url' => 'https://twitter.com/nolan_test/status/930077847535812610/photo/1', + 'id' => 9.300778475358126e17, + 'id_str' => '930077847535812610', + 'indices' => [ + 13, + 36 + ], + 'media_url' => 'http://pbs.twimg.com/media/DOhM30VVwAEpIHq.jpg', + 'media_url_https' => 'https://pbs.twimg.com/media/DOhM30VVwAEpIHq.jpg', + 'sizes' => { + 'thumb' => { + 'h' => 150, + 'resize' => 'crop', + 'w' => 150 + }, + 'large' => { + 'h' => 1366, + 'resize' => 'fit', + 'w' => 2048 + }, + 'medium' => { + 'h' => 800, + 'resize' => 'fit', + 'w' => 1200 + }, + 'small' => { + 'h' => 454, + 'resize' => 'fit', + 'w' => 680 + } + }, + 'type' => 'photo', + 'url' => 'https://t.co/5J1WJSRCy9' + } + }.with_indifferent_access + } + } + }.with_indifferent_access] + end + users do + { + '1' => { + id: '1', + name: 'person 1', + profile_image_url: 'https://chatwoot-assets.local/sample.png' + }, + '2' => { + id: '1', + name: 'person 1', + profile_image_url: 'https://chatwoot-assets.local/sample.png' + } + } + end + + initialize_with { attributes } + end +end diff --git a/spec/lib/webhooks/twitter_spec.rb b/spec/lib/webhooks/twitter_spec.rb index 74820c609..598ef4295 100644 --- a/spec/lib/webhooks/twitter_spec.rb +++ b/spec/lib/webhooks/twitter_spec.rb @@ -9,6 +9,7 @@ describe Webhooks::Twitter do let!(:twitter_channel) { create(:channel_twitter_profile, account: account, profile_id: '1', tweets_enabled: true) } let!(:twitter_inbox) { create(:inbox, channel: twitter_channel, account: account, greeting_enabled: false) } let!(:dm_params) { build(:twitter_message_create_event).with_indifferent_access } + let!(:dm_image_params) { build(:twitter_dm_image_event).with_indifferent_access } let!(:tweet_params) { build(:tweet_create_event).with_indifferent_access } let!(:tweet_params_from_blocked_user) { build(:tweet_create_event, user_has_blocked: true).with_indifferent_access } @@ -22,6 +23,21 @@ describe Webhooks::Twitter do end end + context 'with direct_message attachment params' do + before do + stub_request(:get, 'http://pbs.twimg.com/media/DOhM30VVwAEpIHq.jpg') + .to_return(status: 200, body: '', headers: {}) + end + + it 'creates incoming message with attachments in the twitter inbox' do + twitter_webhook.new(dm_image_params).consume + expect(twitter_inbox.contacts.count).to be 1 + expect(twitter_inbox.conversations.count).to be 1 + expect(twitter_inbox.messages.count).to be 1 + expect(twitter_inbox.messages.last.attachments.count).to be 1 + end + end + context 'with tweet_params params' do it 'does not create incoming message in the twitter inbox if it is a blocked user' do twitter_webhook.new(tweet_params_from_blocked_user).consume From d4be268cc36b0dd0d971625297e9d19c8e22dd2a Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Wed, 13 Apr 2022 12:04:34 +0530 Subject: [PATCH 048/177] enhancement: Remove clickaway for expanded reply box (#4414) * enhancement: Remove clickaway for expanded reply box * Removes unused imports Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> --- .../components/widgets/conversation/MessagesView.vue | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue index edd4e818d..280ddc158 100644 --- a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue +++ b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue @@ -83,7 +83,6 @@ Date: Thu, 14 Apr 2022 13:36:55 +0530 Subject: [PATCH 049/177] feat: Add send message, fix issues with message conditions (#4423) Co-authored-by: Tejaswini --- .../accounts/automation_rules_controller.rb | 13 ++- .../widgets/AutomationActionInput.vue | 21 ++++- .../AutomationActionTeamMessageInput.vue | 62 +++++++++++++ .../dashboard/i18n/locale/en/automation.json | 4 +- .../settings/automation/AddAutomationRule.vue | 6 +- .../automation/EditAutomationRule.vue | 14 ++- .../dashboard/settings/automation/Index.vue | 2 +- .../settings/automation/constants.js | 91 ++++++++++++++----- app/listeners/automation_rule_listener.rb | 2 +- .../automation_notification_mailer.rb | 24 +---- app/models/automation_rule.rb | 2 +- .../automation_rules/action_service.rb | 19 ++-- .../conditions_filter_service.rb | 5 +- .../automation_rules_controller_spec.rb | 30 +++++- .../automation_rule_listener_spec.rb | 22 ++--- 15 files changed, 231 insertions(+), 86 deletions(-) create mode 100644 app/javascript/dashboard/components/widgets/AutomationActionTeamMessageInput.vue diff --git a/app/controllers/api/v1/accounts/automation_rules_controller.rb b/app/controllers/api/v1/accounts/automation_rules_controller.rb index dc4f7b6c8..41296db62 100644 --- a/app/controllers/api/v1/accounts/automation_rules_controller.rb +++ b/app/controllers/api/v1/accounts/automation_rules_controller.rb @@ -20,9 +20,15 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont def show; end def update - @automation_rule.update(automation_rules_permit) - process_attachments - @automation_rule + ActiveRecord::Base.transaction do + @automation_rule.update!(automation_rules_permit) + @automation_rule.actions = params[:actions] if params[:actions] + @automation_rule.save! + process_attachments + rescue StandardError => e + Rails.logger.error e + render json: { error: @automation_rule.errors.messages }.to_json, status: :unprocessable_entity + end end def destroy @@ -45,6 +51,7 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont params[:attachments].each do |uploaded_attachment| @automation_rule.files.attach(uploaded_attachment) end + @automation_rule end def automation_rules_permit diff --git a/app/javascript/dashboard/components/widgets/AutomationActionInput.vue b/app/javascript/dashboard/components/widgets/AutomationActionInput.vue index dc3a31c08..baa66afb4 100644 --- a/app/javascript/dashboard/components/widgets/AutomationActionInput.vue +++ b/app/javascript/dashboard/components/widgets/AutomationActionInput.vue @@ -7,7 +7,7 @@

+ + diff --git a/app/javascript/dashboard/i18n/locale/en/automation.json b/app/javascript/dashboard/i18n/locale/en/automation.json index 8c92467bd..0d2076b65 100644 --- a/app/javascript/dashboard/i18n/locale/en/automation.json +++ b/app/javascript/dashboard/i18n/locale/en/automation.json @@ -89,7 +89,9 @@ "DELETE_MESSAGE": "You need to have atleast one condition to save" }, "ACTION": { - "DELETE_MESSAGE": "You need to have atleast one action to save" + "DELETE_MESSAGE": "You need to have atleast one action to save", + "TEAM_MESSAGE_INPUT_PLACEHOLDER": "Enter your message here", + "TEAM_DROPDOWN_PLACEHOLDER": "Select teams" }, "TOGGLE": { "ACTIVATION_TITLE": "Activate Automation Rule", diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/AddAutomationRule.vue b/app/javascript/dashboard/routes/dashboard/settings/automation/AddAutomationRule.vue index 73a02fd1a..66239b623 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/automation/AddAutomationRule.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/automation/AddAutomationRule.vue @@ -192,10 +192,11 @@ export default { $each: { action_params: { required: requiredIf(prop => { + if (prop.action_name === 'send_email_to_team') return true; return !( prop.action_name === 'mute_conversation' || prop.action_name === 'snooze_conversation' || - prop.action_name === 'resolve_convresation' + prop.action_name === 'resolve_conversation' ); }), }, @@ -361,6 +362,7 @@ export default { getActionDropdownValues(type) { switch (type) { case 'assign_team': + case 'send_email_to_team': return this.$store.getters['teams/getTeams']; case 'add_label': return this.$store.getters['labels/getLabels'].map(i => { @@ -443,6 +445,8 @@ export default { return true; }, showActionInput(actionName) { + if (actionName === 'send_email_to_team' || actionName === 'send_message') + return false; const type = AUTOMATION_ACTION_TYPES.find( action => action.key === actionName ).inputType; diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/EditAutomationRule.vue b/app/javascript/dashboard/routes/dashboard/settings/automation/EditAutomationRule.vue index 14a8e84bb..6c5d03962 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/automation/EditAutomationRule.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/automation/EditAutomationRule.vue @@ -199,7 +199,7 @@ export default { return !( prop.action_name === 'mute_conversation' || prop.action_name === 'snooze_conversation' || - prop.action_name === 'resolve_convresation' + prop.action_name === 'resolve_conversation' ); }), }, @@ -360,6 +360,7 @@ export default { getActionDropdownValues(type) { switch (type) { case 'assign_team': + case 'send_email_to_team': return this.$store.getters['teams/getTeams']; case 'add_label': return this.$store.getters['labels/getLabels'].map(i => { @@ -475,6 +476,15 @@ export default { actionParams = [ ...this.getActionDropdownValues(action.action_name), ].filter(item => [...action.action_params].includes(item.id)); + } else if (inputType === 'team_message') { + actionParams = { + team_ids: [ + ...this.getActionDropdownValues(action.action_name), + ].filter(item => + [...action.action_params[0].team_ids].includes(item.id) + ), + message: action.action_params[0].message, + }; } else actionParams = [...action.action_params]; } return { @@ -489,6 +499,8 @@ export default { }; }, showActionInput(actionName) { + if (actionName === 'send_email_to_team' || actionName === 'send_message') + return false; const type = AUTOMATION_ACTION_TYPES.find( action => action.key === actionName ).inputType; diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue index f028933cb..28b8c982f 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue @@ -253,7 +253,7 @@ export default { : this.$t('AUTOMATION.TOGGLE.ACTIVATION_DESCRIPTION', { automationName: automation.name, }); - // Check if uses confirms to proceed + // Check if user confirms to proceed const ok = await this.$refs.confirmDialog.showConfirmation(); if (ok) { await await this.$store.dispatch('automations/update', { diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js b/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js index cd306007a..bdec6ebc0 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js +++ b/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js @@ -76,11 +76,16 @@ export const AUTOMATIONS = { name: 'Add a label', attributeI18nKey: 'ADD_LABEL', }, - // { - // key: 'send_email_to_team', - // name: 'Send an email to team', - // attributeI18nKey: 'SEND_MESSAGE', - // }, + { + key: 'send_email_to_team', + name: 'Send an email to team', + attributeI18nKey: 'SEND_EMAIL_TO_TEAM', + }, + { + key: 'send_message', + name: 'Send a message', + attributeI18nKey: 'SEND_MESSAGE', + }, { key: 'send_email_transcript', name: 'Send an email transcript', @@ -96,8 +101,9 @@ export const AUTOMATIONS = { name: 'Snooze conversation', attributeI18nKey: 'MUTE_CONVERSATION', }, + { - key: 'resolve_convresation', + key: 'resolve_conversation', name: 'Resolve conversation', attributeI18nKey: 'RESOLVE_CONVERSATION', }, @@ -106,6 +112,11 @@ export const AUTOMATIONS = { name: 'Send Webhook Event', attributeI18nKey: 'SEND_WEBHOOK_EVENT', }, + // { + // key: 'send_attachment', + // name: 'Send Attachment', + // attributeI18nKey: 'SEND_ATTACHMENT', + // }, ], }, conversation_created: { @@ -132,7 +143,7 @@ export const AUTOMATIONS = { filterOperators: OPERATOR_TYPES_1, }, { - key: 'referrer', + key: 'referer', name: 'Referrer Link', attributeI18nKey: 'REFERER_LINK', inputType: 'plain_text', @@ -150,11 +161,16 @@ export const AUTOMATIONS = { name: 'Assign an agent', attributeI18nKey: 'ASSIGN_AGENT', }, - // { - // key: 'send_email_to_team', - // name: 'Send an email to team', - // attributeI18nKey: 'SEND_MESSAGE', - // }, + { + key: 'send_email_to_team', + name: 'Send an email to team', + attributeI18nKey: 'SEND_EMAIL_TO_TEAM', + }, + { + key: 'send_message', + name: 'Send a message', + attributeI18nKey: 'SEND_MESSAGE', + }, { key: 'send_email_transcript', name: 'Send an email transcript', @@ -171,7 +187,7 @@ export const AUTOMATIONS = { attributeI18nKey: 'MUTE_CONVERSATION', }, { - key: 'resolve_convresation', + key: 'resolve_conversation', name: 'Resolve conversation', attributeI18nKey: 'RESOLVE_CONVERSATION', }, @@ -180,6 +196,11 @@ export const AUTOMATIONS = { name: 'Send Webhook Event', attributeI18nKey: 'SEND_WEBHOOK_EVENT', }, + // { + // key: 'send_attachment', + // name: 'Send Attachment', + // attributeI18nKey: 'SEND_ATTACHMENT', + // }, ], }, conversation_updated: { @@ -238,11 +259,16 @@ export const AUTOMATIONS = { name: 'Assign an agent', attributeI18nKey: 'ASSIGN_AGENT', }, - // { - // key: 'send_email_to_team', - // name: 'Send an email to team', - // attributeI18nKey: 'SEND_MESSAGE', - // }, + { + key: 'send_email_to_team', + name: 'Send an email to team', + attributeI18nKey: 'SEND_EMAIL_TO_TEAM', + }, + { + key: 'send_message', + name: 'Send a message', + attributeI18nKey: 'SEND_MESSAGE', + }, { key: 'send_email_transcript', name: 'Send an email transcript', @@ -259,7 +285,7 @@ export const AUTOMATIONS = { attributeI18nKey: 'MUTE_CONVERSATION', }, { - key: 'resolve_convresation', + key: 'resolve_conversation', name: 'Resolve conversation', attributeI18nKey: 'RESOLVE_CONVERSATION', }, @@ -268,6 +294,11 @@ export const AUTOMATIONS = { name: 'Send Webhook Event', attributeI18nKey: 'SEND_WEBHOOK_EVENT', }, + // { + // key: 'send_attachment', + // name: 'Send Attachment', + // attributeI18nKey: 'SEND_ATTACHMENT', + // }, ], }, }; @@ -298,11 +329,11 @@ export const AUTOMATION_ACTION_TYPES = [ label: 'Add a label', inputType: 'multi_select', }, - // { - // key: 'send_email_to_team', - // label: 'Send an email to team', - // inputType: 'multi_select', - // }, + { + key: 'send_email_to_team', + label: 'Send an email to team', + inputType: 'team_message', + }, { key: 'send_email_transcript', label: 'Send an email transcript', @@ -319,7 +350,7 @@ export const AUTOMATION_ACTION_TYPES = [ inputType: null, }, { - key: 'resolve_convresation', + key: 'resolve_conversation', label: 'Resolve conversation', inputType: null, }, @@ -328,4 +359,14 @@ export const AUTOMATION_ACTION_TYPES = [ label: 'Send Webhook Event', inputType: 'url', }, + // { + // key: 'send_attachment', + // label: 'Send Attachment', + // inputType: 'file', + // }, + { + key: 'send_message', + label: 'Send a message', + inputType: 'textarea', + }, ]; diff --git a/app/listeners/automation_rule_listener.rb b/app/listeners/automation_rule_listener.rb index c8c021a4e..6ebc46666 100644 --- a/app/listeners/automation_rule_listener.rb +++ b/app/listeners/automation_rule_listener.rb @@ -36,7 +36,7 @@ class AutomationRuleListener < BaseListener return unless rule_present?('message_created', account) @rules.each do |rule| - conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, message.conversation).message_conditions + conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, message.conversation, { message: message }).message_conditions ::AutomationRules::ActionService.new(rule, account, message.conversation).perform if conditions_match.present? end end diff --git a/app/mailers/team_notifications/automation_notification_mailer.rb b/app/mailers/team_notifications/automation_notification_mailer.rb index d6ca47f81..f1bf1ab40 100644 --- a/app/mailers/team_notifications/automation_notification_mailer.rb +++ b/app/mailers/team_notifications/automation_notification_mailer.rb @@ -7,29 +7,7 @@ class TeamNotifications::AutomationNotificationMailer < ApplicationMailer @custom_message = message @action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id) - send_an_email_to_team and return - end - - def conversation_updated(conversation, team, message) - return unless smtp_config_set_or_development? - - @agents = team.team_members - @conversation = conversation - @custom_message = message - @action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id) - - send_an_email_to_team and return - end - - def message_created(conversation, team, message) - return unless smtp_config_set_or_development? - - @agents = team.team_members - @conversation = conversation - @custom_message = message - @action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id) - - send_an_email_to_team and return + send_an_email_to_team end private diff --git a/app/models/automation_rule.rb b/app/models/automation_rule.rb index 95cd9543e..1dcc2583d 100644 --- a/app/models/automation_rule.rb +++ b/app/models/automation_rule.rb @@ -27,7 +27,7 @@ class AutomationRule < ApplicationRecord scope :active, -> { where(active: true) } - CONDITIONS_ATTRS = %w[content email country_code status message_type browser_language assignee_id team_id referrer city company].freeze + CONDITIONS_ATTRS = %w[content email country_code status message_type browser_language assignee_id team_id referer city company].freeze ACTIONS_ATTRS = %w[send_message add_label send_email_to_team assign_team assign_best_agents send_attachments].freeze private diff --git a/app/services/automation_rules/action_service.rb b/app/services/automation_rules/action_service.rb index 1249bd9c5..0771f817d 100644 --- a/app/services/automation_rules/action_service.rb +++ b/app/services/automation_rules/action_service.rb @@ -22,8 +22,6 @@ class AutomationRules::ActionService private def send_attachments(_file_params) - return if @rule.event_name == 'message_created' - blobs = @rule.files.map { |file, _| file.blob } params = { content: nil, private: false, attachments: blobs } mb = Messages::MessageBuilder.new(nil, @conversation, params) @@ -44,6 +42,10 @@ class AutomationRules::ActionService @conversation.snoozed! end + def resolve_conversation(_params) + @conversation.resolved! + end + def change_status(status) @conversation.update!(status: status[0]) end @@ -54,8 +56,6 @@ class AutomationRules::ActionService end def send_message(message) - return if @rule.event_name == 'message_created' - params = { content: message[0], private: false } mb = Messages::MessageBuilder.new(nil, @conversation, params) mb.perform @@ -82,15 +82,10 @@ class AutomationRules::ActionService end def send_email_to_team(params) - team = Team.find(params[:team_ids][0]) + teams = Team.where(id: params[0][:team_ids]) - case @rule.event_name - when 'conversation_created', 'conversation_status_changed' - TeamNotifications::AutomationNotificationMailer.conversation_creation(@conversation, team, params[:message])&.deliver_now - when 'conversation_updated' - TeamNotifications::AutomationNotificationMailer.conversation_updated(@conversation, team, params[:message])&.deliver_now - when 'message_created' - TeamNotifications::AutomationNotificationMailer.message_created(@conversation, team, params[:message])&.deliver_now + teams.each do |team| + TeamNotifications::AutomationNotificationMailer.conversation_creation(@conversation, team, params[0][:message])&.deliver_now end end diff --git a/app/services/automation_rules/conditions_filter_service.rb b/app/services/automation_rules/conditions_filter_service.rb index 46988771d..2ac011fbd 100644 --- a/app/services/automation_rules/conditions_filter_service.rb +++ b/app/services/automation_rules/conditions_filter_service.rb @@ -3,13 +3,14 @@ require 'json' class AutomationRules::ConditionsFilterService < FilterService ATTRIBUTE_MODEL = 'contact_attribute'.freeze - def initialize(rule, conversation = nil) + def initialize(rule, conversation = nil, options = {}) super([], nil) @rule = rule @conversation = conversation @account = conversation.account file = File.read('./lib/filters/filter_keys.json') @filters = JSON.parse(file) + @options = options end def perform @@ -41,7 +42,7 @@ class AutomationRules::ConditionsFilterService < FilterService current_filter = message_filters[query_hash['attribute_key']] @query_string += message_query_string(current_filter, query_hash.with_indifferent_access, current_index) end - records = Message.where(conversation: @conversation).where(@query_string, @filter_values.with_indifferent_access) + records = Message.where(id: @options[:message].id).where(@query_string, @filter_values.with_indifferent_access) records.any? end diff --git a/spec/controllers/api/v1/accounts/automation_rules_controller_spec.rb b/spec/controllers/api/v1/accounts/automation_rules_controller_spec.rb index 0b921bc07..2039daf15 100644 --- a/spec/controllers/api/v1/accounts/automation_rules_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/automation_rules_controller_spec.rb @@ -259,17 +259,41 @@ RSpec.describe 'Api::V1::Accounts::AutomationRulesController', type: :request do end context 'when it is an authenticated user' do - it 'returns for updated automation_rule for account' do - params = { name: 'Update name' } + let(:update_params) do + { + description: 'Update description', + name: 'Update name', + conditions: [ + { + attribute_key: 'browser_language', + filter_operator: 'equal_to', + values: ['en'], + query_operator: 'AND' + } + ], + actions: [ + { + action_name: :update_additional_attributes, + action_params: [{ intiated_at: '2021-12-03 17:25:26.844536 +0530' }] + } + ] + }.with_indifferent_access + end + + it 'returns for cloned automation_rule for account' do expect(account.automation_rules.count).to eq(1) + expect(account.automation_rules.first.actions.size).to eq(4) patch "/api/v1/accounts/#{account.id}/automation_rules/#{automation_rule.id}", headers: administrator.create_new_auth_token, - params: params + params: update_params expect(response).to have_http_status(:success) body = JSON.parse(response.body, symbolize_names: true) expect(body[:payload][:name]).to eq('Update name') + expect(body[:payload][:description]).to eq('Update description') + expect(body[:payload][:conditions].size).to eq(1) + expect(body[:payload][:actions].size).to eq(1) end it 'returns for updated active flag for automation_rule' do diff --git a/spec/listeners/automation_rule_listener_spec.rb b/spec/listeners/automation_rule_listener_spec.rb index b0ed4d0bd..6bbffe077 100644 --- a/spec/listeners/automation_rule_listener_spec.rb +++ b/spec/listeners/automation_rule_listener_spec.rb @@ -30,10 +30,10 @@ describe AutomationRuleListener do automation_rule.update!(actions: [ { - 'action_name' => 'send_email_to_team', 'action_params' => { + '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] }, @@ -104,7 +104,7 @@ describe AutomationRuleListener do it 'triggers automation rule send email transcript to the mentioned email' do mailer = double - expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_updated) + expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) listener.conversation_updated(event) @@ -116,7 +116,7 @@ describe AutomationRuleListener do it 'triggers automation rule send message to the contacts' do expect(conversation.messages).to be_empty - expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_updated) + expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) listener.conversation_updated(event) @@ -202,7 +202,7 @@ describe AutomationRuleListener do automation_rule - expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_updated) + expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) listener.conversation_updated(event) @@ -214,7 +214,7 @@ describe AutomationRuleListener do it 'triggers automation rule send email to the team' do automation_rule - expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_updated) + expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) listener.conversation_updated(event) end @@ -224,7 +224,7 @@ describe AutomationRuleListener do automation_rule - expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_updated) + expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) listener.conversation_updated(event) @@ -256,7 +256,7 @@ describe AutomationRuleListener do automation_rule - expect(TeamNotifications::AutomationNotificationMailer).to receive(:message_created) + expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) listener.message_created(event) @@ -269,7 +269,7 @@ describe AutomationRuleListener do automation_rule - expect(TeamNotifications::AutomationNotificationMailer).to receive(:message_created) + expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) listener.message_created(event) @@ -281,7 +281,7 @@ describe AutomationRuleListener do expect(conversation.assignee).to be_nil automation_rule - expect(TeamNotifications::AutomationNotificationMailer).to receive(:message_created) + expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) listener.message_created(event) @@ -295,7 +295,7 @@ describe AutomationRuleListener do automation_rule - expect(TeamNotifications::AutomationNotificationMailer).to receive(:message_created) + expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) listener.message_created(event) From 80e5d6d7a0fdf03b3a313b3ea679d42aac06ce7e Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Thu, 14 Apr 2022 17:15:57 +0530 Subject: [PATCH 050/177] feat: add chatwoot_edition variable for CE docker images (#4462) * chore: add chatwoot_edition variable for CE docker images * fix cw_edition variable * chore: update comment * feat: include cw_edition data in payload to hub * refactor cw_edition to edition --- .github/workflows/publish_foss_docker.yml | 13 ++++++++++--- lib/chatwoot_hub.rb | 3 ++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish_foss_docker.yml b/.github/workflows/publish_foss_docker.yml index f56d67f99..37f0f3e6e 100644 --- a/.github/workflows/publish_foss_docker.yml +++ b/.github/workflows/publish_foss_docker.yml @@ -1,7 +1,7 @@ # # # # This action will publish Chatwoot CE docker image. # # This is set to run against merges to develop, master -# # when tags are created. +# # and when tags are created. # # name: Publish Chatwoot CE docker images @@ -20,8 +20,10 @@ jobs: env: GIT_REF: ${{ github.head_ref || github.ref_name }} # ref_name to get tags/branches steps: - - - name: Set up QEMU + - name: Checkout + uses: actions/checkout@v2 + + - name: Set up QEMU uses: docker/setup-qemu-action@v1 - name: Set up Docker Buildx @@ -32,6 +34,10 @@ jobs: rm -rf enterprise rm -rf spec/enterprise + - name: Set Chatwoot edition + run: | + echo -en '\nENV CW_EDITION="ce"' >> docker/Dockerfile + - name: set docker tag run: | echo "DOCKER_TAG=chatwoot/chatwoot:$GIT_REF-ce" >> $GITHUB_ENV @@ -50,6 +56,7 @@ jobs: - name: Build and push uses: docker/build-push-action@v2 with: + context: . file: docker/Dockerfile push: true tags: ${{ env.DOCKER_TAG }} diff --git a/lib/chatwoot_hub.rb b/lib/chatwoot_hub.rb index 655060c7e..86f296f79 100644 --- a/lib/chatwoot_hub.rb +++ b/lib/chatwoot_hub.rb @@ -16,7 +16,8 @@ class ChatwootHub installation_identifier: installation_identifier, installation_version: Chatwoot.config[:version], installation_host: URI.parse(ENV.fetch('FRONTEND_URL', '')).host, - installation_env: ENV.fetch('INSTALLATION_ENV', '') + installation_env: ENV.fetch('INSTALLATION_ENV', ''), + edition: ENV.fetch('CW_EDITION', '') } end From 0319b78eacce64ffa0d6751b530475ad8ab4c503 Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Thu, 14 Apr 2022 20:54:26 +0530 Subject: [PATCH 051/177] fix: Allow users to login even if they have access to more than 15 accounts (#4475) --- app/javascript/dashboard/App.vue | 57 +++++--- app/javascript/dashboard/api/auth.js | 36 +---- .../components/layout/AvailabilityStatus.vue | 5 +- app/javascript/dashboard/helper/APIHelper.js | 3 +- app/javascript/dashboard/helper/URLHelper.js | 12 +- .../dashboard/helper/actionCable.js | 11 +- app/javascript/dashboard/helper/pushHelper.js | 2 +- .../dashboard/helper/specs/URLHelper.spec.js | 15 +- .../dashboard/routes/dashboard/Dashboard.vue | 1 - app/javascript/dashboard/routes/index.js | 84 ++++++----- app/javascript/dashboard/routes/index.spec.js | 131 ++++++++++------- .../dashboard/store/modules/auth.js | 134 +++++++----------- .../store/modules/conversations/getters.js | 5 +- .../store/modules/specs/auth/actions.spec.js | 42 ++++-- .../store/modules/specs/auth/getters.spec.js | 119 ++++++++++------ .../modules/specs/auth/mutations.spec.js | 24 ++++ .../dashboard/store/mutation-types.js | 2 +- app/javascript/dashboard/store/utils/api.js | 19 +-- app/javascript/packs/application.js | 16 +-- 19 files changed, 368 insertions(+), 350 deletions(-) diff --git a/app/javascript/dashboard/App.vue b/app/javascript/dashboard/App.vue index 2c91de16f..1e262f1ff 100644 --- a/app/javascript/dashboard/App.vue +++ b/app/javascript/dashboard/App.vue @@ -1,5 +1,5 @@ + diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/PreChatForm/Settings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/PreChatForm/Settings.vue index 6c01bc3dd..184e16e0f 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/PreChatForm/Settings.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/PreChatForm/Settings.vue @@ -1,10 +1,10 @@ + + + + diff --git a/app/javascript/dashboard/components/widgets/conversation/WhatsappTemplates/TemplatesPicker.vue b/app/javascript/dashboard/components/widgets/conversation/WhatsappTemplates/TemplatesPicker.vue new file mode 100644 index 000000000..b9e9ba71c --- /dev/null +++ b/app/javascript/dashboard/components/widgets/conversation/WhatsappTemplates/TemplatesPicker.vue @@ -0,0 +1,163 @@ + + + + + diff --git a/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue b/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue index c1c3440bc..7f454fa20 100644 --- a/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue +++ b/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue @@ -7,7 +7,7 @@ @@ -47,6 +48,10 @@ export default { type: Boolean, deafaut: false, }, + styles: { + type: Object, + default: () => {}, + }, }, methods: { onChange(e) { diff --git a/app/javascript/dashboard/i18n/locale/en/index.js b/app/javascript/dashboard/i18n/locale/en/index.js index b21009322..d49420335 100644 --- a/app/javascript/dashboard/i18n/locale/en/index.js +++ b/app/javascript/dashboard/i18n/locale/en/index.js @@ -21,6 +21,7 @@ import { default as _setNewPassword } from './setNewPassword.json'; import { default as _settings } from './settings.json'; import { default as _signup } from './signup.json'; import { default as _teamsSettings } from './teamsSettings.json'; +import { default as _whatsappTemplates } from './whatsappTemplates.json'; import { default as _bulkActions } from './bulkActions.json'; export default { @@ -47,5 +48,6 @@ export default { ..._settings, ..._signup, ..._teamsSettings, + ..._whatsappTemplates, ..._bulkActions, }; diff --git a/app/javascript/dashboard/i18n/locale/en/whatsappTemplates.json b/app/javascript/dashboard/i18n/locale/en/whatsappTemplates.json new file mode 100644 index 000000000..bbcf28156 --- /dev/null +++ b/app/javascript/dashboard/i18n/locale/en/whatsappTemplates.json @@ -0,0 +1,25 @@ +{ + "WHATSAPP_TEMPLATES": { + "MODAL": { + "TITLE": "Whatsapp Templates", + "SUBTITLE": "Select the whatsapp template you want to send", + "TEMPLATE_SELECTED_SUBTITLE": "Process %{templateName}" + }, + "PICKER": { + "SEARCH_PLACEHOLDER": "Search Templates", + "NO_TEMPLATES_FOUND": "No templates found for", + "LABELS": { + "LANGUAGE": "Language", + "TEMPLATE_BODY": "Template Body", + "CATEGORY": "Category" + } + }, + "PARSER": { + "VARIABLES_LABEL": "Variables", + "VARIABLE_PLACEHOLDER": "Enter %{variable} value", + "GO_BACK_LABEL": "Go Back", + "SEND_MESSAGE_LABEL": "Send Message", + "FORM_ERROR_MESSAGE": "Please fill all variables before sending" + } + } +} diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue index f2d262644..9dda5c02c 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue @@ -1,12 +1,12 @@ diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue index ee12b989a..7b90ef6eb 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue @@ -11,14 +11,13 @@ @mouseleave="onCardLeave" @click="cardClick(chat)" > -

+
+ + + +
{{ $t('BULK_ACTION.AGENT_SELECT_LABEL') }} diff --git a/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/UpdateActions.vue b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/UpdateActions.vue new file mode 100644 index 000000000..f6bd53a9a --- /dev/null +++ b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/UpdateActions.vue @@ -0,0 +1,166 @@ + + + + + diff --git a/app/javascript/dashboard/i18n/locale/en/bulkActions.json b/app/javascript/dashboard/i18n/locale/en/bulkActions.json index bfd688bef..f9e084cd7 100644 --- a/app/javascript/dashboard/i18n/locale/en/bulkActions.json +++ b/app/javascript/dashboard/i18n/locale/en/bulkActions.json @@ -1,17 +1,22 @@ { - "BULK_ACTION": { - "CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected", - "AGENT_SELECT_LABEL": "Select Agent", - "ASSIGN_CONFIRMATION_LABEL": "Are you sure you want to assign %{conversationCount} %{conversationLabel} to", - "GO_BACK_LABEL": "Go back", - "ASSIGN_LABEL": "Assign", - "ASSIGN_AGENT_TOOLTIP": "Assign Agent", - "RESOLVE_TOOLTIP": "Resolve", - "ASSIGN_SUCCESFUL": "Conversations assigned successfully", - "ASSIGN_FAILED": "Failed to assign conversations, please try again", - "RESOLVE_SUCCESFUL": "Conversations resolved successfully", - "RESOLVE_FAILED": "Failed to resolve conversations, please try again", - "ALL_CONVERSATIONS_SELECTED_ALERT": "Conversations visible on this page are only selected.", - "AGENT_LIST_LOADING": "Loading Agents" + "BULK_ACTION": { + "CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected", + "AGENT_SELECT_LABEL": "Select Agent", + "ASSIGN_CONFIRMATION_LABEL": "Are you sure you want to assign %{conversationCount} %{conversationLabel} to", + "GO_BACK_LABEL": "Go back", + "ASSIGN_LABEL": "Assign", + "ASSIGN_AGENT_TOOLTIP": "Assign Agent", + "ASSIGN_SUCCESFUL": "Conversations assigned successfully", + "ASSIGN_FAILED": "Failed to assign conversations, please try again", + "RESOLVE_SUCCESFUL": "Conversations resolved successfully", + "RESOLVE_FAILED": "Failed to resolve conversations, please try again", + "ALL_CONVERSATIONS_SELECTED_ALERT": "Conversations visible on this page are only selected.", + "AGENT_LIST_LOADING": "Loading Agents", + "UPDATE": { + "CHANGE_STATUS": "Change status", + "SNOOZE_UNTIL_NEXT_REPLY": "Snooze until next reply", + "UPDATE_SUCCESFUL": "Conversation status updated successfully.", + "UPDATE_FAILED": "Failed to update conversations, please try again" } + } } diff --git a/app/javascript/shared/assets/stylesheets/z-index.scss b/app/javascript/shared/assets/stylesheets/z-index.scss index 011bdf11c..43c28d575 100644 --- a/app/javascript/shared/assets/stylesheets/z-index.scss +++ b/app/javascript/shared/assets/stylesheets/z-index.scss @@ -2,6 +2,7 @@ // z-index --z-index-minus: -1; --z-index-zero: 0; + --z-index-one: 1; --z-index-low: 10; --z-index-twenty: 20; --z-index-normal: 100; diff --git a/app/javascript/shared/components/FluentIcon/dashboard-icons.json b/app/javascript/shared/components/FluentIcon/dashboard-icons.json index 1f4a35b2f..518a44cac 100644 --- a/app/javascript/shared/components/FluentIcon/dashboard-icons.json +++ b/app/javascript/shared/components/FluentIcon/dashboard-icons.json @@ -103,6 +103,7 @@ "power-outline": "M8.204 4.82a.75.75 0 0 1 .634 1.36A7.51 7.51 0 0 0 4.5 12.991c0 4.148 3.358 7.51 7.499 7.51s7.499-3.362 7.499-7.51a7.51 7.51 0 0 0-4.323-6.804.75.75 0 1 1 .637-1.358 9.01 9.01 0 0 1 5.186 8.162c0 4.976-4.029 9.01-9 9.01C7.029 22 3 17.966 3 12.99a9.01 9.01 0 0 1 5.204-8.17ZM12 2.496a.75.75 0 0 1 .743.648l.007.102v7.5a.75.75 0 0 1-1.493.102l-.007-.102v-7.5a.75.75 0 0 1 .75-.75Z", "quote-outline": "M7.5 6a2.5 2.5 0 0 1 2.495 2.336l.005.206c-.01 3.555-1.24 6.614-3.705 9.223a.75.75 0 1 1-1.09-1.03c1.64-1.737 2.66-3.674 3.077-5.859A2.5 2.5 0 1 1 7.5 6Zm9 0a2.5 2.5 0 0 1 2.495 2.336l.005.206c-.01 3.56-1.238 6.614-3.705 9.223a.75.75 0 1 1-1.09-1.03c1.643-1.738 2.662-3.672 3.078-5.859A2.5 2.5 0 1 1 16.5 6Zm-9 1.5a1 1 0 1 0 .993 1.117l.007-.124a1 1 0 0 0-1-.993Zm9 0a1 1 0 1 0 .993 1.117l.007-.124a1 1 0 0 0-1-.993Z", "resize-large-outline": "M6.25 4.5A1.75 1.75 0 0 0 4.5 6.25v1.5a.75.75 0 0 1-1.5 0v-1.5A3.25 3.25 0 0 1 6.25 3h1.5a.75.75 0 0 1 0 1.5h-1.5ZM19.5 6.25a1.75 1.75 0 0 0-1.75-1.75h-1.5a.75.75 0 0 1 0-1.5h1.5A3.25 3.25 0 0 1 21 6.25v1.5a.75.75 0 0 1-1.5 0v-1.5ZM19.5 17.75a1.75 1.75 0 0 1-1.75 1.75h-1.5a.75.75 0 0 0 0 1.5h1.5A3.25 3.25 0 0 0 21 17.75v-1.5a.75.75 0 0 0-1.5 0v1.5ZM4.5 17.75c0 .966.784 1.75 1.75 1.75h1.5a.75.75 0 0 1 0 1.5h-1.5A3.25 3.25 0 0 1 3 17.75v-1.5a.75.75 0 0 1 1.5 0v1.5ZM8.25 6A2.25 2.25 0 0 0 6 8.25v7.5A2.25 2.25 0 0 0 8.25 18h7.5A2.25 2.25 0 0 0 18 15.75v-7.5A2.25 2.25 0 0 0 15.75 6h-7.5ZM7.5 8.25a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 .75.75v7.5a.75.75 0 0 1-.75.75h-7.5a.75.75 0 0 1-.75-.75v-7.5Z", + "repeat-outline": "m14.712 2.289l-.087-.078a1 1 0 0 0-1.327.078l-.078.087a.999.999 0 0 0 .078 1.326l1.299 1.297H8.999l-.24.004A6.997 6.997 0 0 0 2 11.993a6.94 6.94 0 0 0 1.189 3.899a.999.999 0 0 0 1.626-1.163l-.135-.218A4.997 4.997 0 0 1 9 6.998h5.595l-1.297 1.297l-.078.087a.999.999 0 0 0 1.492 1.326l3.006-3.003l.077-.087a.999.999 0 0 0-.078-1.326l-3.005-3.003Zm6.075 5.771A.999.999 0 0 0 19 8.677c0 .209.064.402.172.561a4.997 4.997 0 0 1-4.17 7.75H9.414l1.294-1.29l.083-.096a1 1 0 0 0-.006-1.23l-.077-.088l-.095-.084a1.001 1.001 0 0 0-1.232.006l-.088.078l-3.005 3.003l-.083.095a1 1 0 0 0 .006 1.231l.077.087l3.005 3.003l.095.084a1 1 0 0 0 1.397-1.41l-.077-.087l-1.304-1.303H15l.24-.003a6.997 6.997 0 0 0 5.546-10.927v.003Z", "save-outline": "M3 5.75A2.75 2.75 0 0 1 5.75 3h9.964a3.25 3.25 0 0 1 2.299.952l2.035 2.035c.61.61.952 1.437.952 2.299v9.964A2.75 2.75 0 0 1 18.25 21H5.75A2.75 2.75 0 0 1 3 18.25V5.75ZM5.75 4.5c-.69 0-1.25.56-1.25 1.25v12.5c0 .69.56 1.25 1.25 1.25H6v-5.25A2.25 2.25 0 0 1 8.25 12h7.5A2.25 2.25 0 0 1 18 14.25v5.25h.25c.69 0 1.25-.56 1.25-1.25V8.286c0-.465-.184-.91-.513-1.238l-2.035-2.035a1.75 1.75 0 0 0-.952-.49V7.25a2.25 2.25 0 0 1-2.25 2.25h-4.5A2.25 2.25 0 0 1 7 7.25V4.5H5.75Zm10.75 15v-5.25a.75.75 0 0 0-.75-.75h-7.5a.75.75 0 0 0-.75.75v5.25h9Zm-8-15v2.75c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75V4.5h-6Z", "search-outline": "M10 2.75a7.25 7.25 0 0 1 5.63 11.819l4.9 4.9a.75.75 0 0 1-.976 1.134l-.084-.073-4.901-4.9A7.25 7.25 0 1 1 10 2.75Zm0 1.5a5.75 5.75 0 1 0 0 11.5 5.75 5.75 0 0 0 0-11.5Z", "send-outline": "M5.694 12 2.299 3.272c-.236-.607.356-1.188.942-.982l.093.04 18 9a.75.75 0 0 1 .097 1.283l-.097.058-18 9c-.583.291-1.217-.244-1.065-.847l.03-.096L5.694 12 2.299 3.272 5.694 12ZM4.402 4.54l2.61 6.71h6.627a.75.75 0 0 1 .743.648l.007.102a.75.75 0 0 1-.649.743l-.101.007H7.01l-2.609 6.71L19.322 12 4.401 4.54Z", From ae72757d23da8ce4aa0f65145469c6bcb0844eb8 Mon Sep 17 00:00:00 2001 From: Tejaswini Chile Date: Mon, 13 Jun 2022 15:56:49 +0530 Subject: [PATCH 162/177] feat: APIs for Articles (#4777) Fixes: #4802 --- Gemfile | 3 + Gemfile.lock | 6 +- .../api/v1/accounts/articles_controller.rb | 48 ++++++ .../api/v1/accounts/categories_controller.rb | 4 +- app/models/article.rb | 67 +++++++-- app/models/category.rb | 37 +++-- app/models/user.rb | 1 + .../accounts/articles/_article.json.jbuilder | 20 +++ .../v1/accounts/articles/create.json.jbuilder | 3 + .../v1/accounts/articles/edit.json.jbuilder | 3 + .../v1/accounts/articles/index.json.jbuilder | 3 + .../v1/accounts/articles/show.json.jbuilder | 3 + .../v1/accounts/articles/update.json.jbuilder | 3 + .../categories/_category.json.jbuilder | 2 + config/routes.rb | 2 +- ...20527080906_add_reference_for_author_id.rb | 6 + ...6_add_index_on_category_slug_and_locale.rb | 7 + db/schema.rb | 6 +- .../v1/accounts/articles_controller_spec.rb | 137 ++++++++++++++++++ .../v1/accounts/categories_controller_spec.rb | 52 +++++-- spec/factories/articles.rb | 4 +- spec/models/article_spec.rb | 101 +++++++++++++ spec/models/category_spec.rb | 35 +++++ 23 files changed, 511 insertions(+), 42 deletions(-) create mode 100644 app/controllers/api/v1/accounts/articles_controller.rb create mode 100644 app/views/api/v1/accounts/articles/_article.json.jbuilder create mode 100644 app/views/api/v1/accounts/articles/create.json.jbuilder create mode 100644 app/views/api/v1/accounts/articles/edit.json.jbuilder create mode 100644 app/views/api/v1/accounts/articles/index.json.jbuilder create mode 100644 app/views/api/v1/accounts/articles/show.json.jbuilder create mode 100644 app/views/api/v1/accounts/articles/update.json.jbuilder create mode 100644 db/migrate/20220527080906_add_reference_for_author_id.rb create mode 100644 db/migrate/20220527120826_add_index_on_category_slug_and_locale.rb create mode 100644 spec/controllers/api/v1/accounts/articles_controller_spec.rb diff --git a/Gemfile b/Gemfile index b8e511b1b..9029ccbcb 100644 --- a/Gemfile +++ b/Gemfile @@ -128,6 +128,9 @@ gem 'html2text' # to calculate working hours gem 'working_hours' +# full text search for articles +gem 'pg_search' + group :production, :staging do # we dont want request timing out in development while using byebug gem 'rack-timeout' diff --git a/Gemfile.lock b/Gemfile.lock index de9443f29..cff30308b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -388,6 +388,9 @@ GEM parser (3.1.1.0) ast (~> 2.4.1) pg (1.3.2) + pg_search (2.3.6) + activerecord (>= 5.2) + activesupport (>= 5.2) procore-sift (0.16.0) rails (> 4.2.0) pry (0.14.1) @@ -694,6 +697,7 @@ DEPENDENCIES mock_redis newrelic_rpm pg + pg_search procore-sift pry-rails puma @@ -742,4 +746,4 @@ RUBY VERSION ruby 3.0.4p208 BUNDLED WITH - 2.3.14 + 2.3.15 diff --git a/app/controllers/api/v1/accounts/articles_controller.rb b/app/controllers/api/v1/accounts/articles_controller.rb new file mode 100644 index 000000000..232eecd32 --- /dev/null +++ b/app/controllers/api/v1/accounts/articles_controller.rb @@ -0,0 +1,48 @@ +class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController + before_action :portal + before_action :fetch_article, except: [:index, :create] + + def index + @articles = @portal.articles + @articles.search(list_params) if params[:payload].present? + end + + def create + @article = @portal.articles.create!(article_params) + end + + def edit; end + + def show; end + + def update + @article.update!(article_params) + end + + def destroy + @article.destroy! + head :ok + end + + private + + def fetch_article + @article = @portal.articles.find(params[:id]) + end + + def portal + @portal ||= Current.account.portals.find_by(slug: params[:portal_id]) + end + + def article_params + params.require(:article).permit( + :title, :content, :description, :position, :category_id, :author_id + ) + end + + def list_params + params.require(:payload).permit( + :category_slug, :locale, :query + ) + end +end diff --git a/app/controllers/api/v1/accounts/categories_controller.rb b/app/controllers/api/v1/accounts/categories_controller.rb index 246eeb2a2..a77f1fb2a 100644 --- a/app/controllers/api/v1/accounts/categories_controller.rb +++ b/app/controllers/api/v1/accounts/categories_controller.rb @@ -3,7 +3,7 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle before_action :fetch_category, except: [:index, :create] def index - @categories = @portal.categories + @categories = @portal.categories.search(params) end def create @@ -31,7 +31,7 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle def category_params params.require(:category).permit( - :name, :description, :position + :name, :description, :position, :slug, :locale ) end end diff --git a/app/models/article.rb b/app/models/article.rb index 750e3a4fd..72bf3b2c4 100644 --- a/app/models/article.rb +++ b/app/models/article.rb @@ -2,25 +2,34 @@ # # Table name: articles # -# id :bigint not null, primary key -# content :text -# description :text -# status :integer -# title :string -# views :integer -# created_at :datetime not null -# updated_at :datetime not null -# account_id :integer not null -# author_id :integer -# category_id :integer -# folder_id :integer -# portal_id :integer not null +# id :bigint not null, primary key +# content :text +# description :text +# status :integer +# title :string +# views :integer +# created_at :datetime not null +# updated_at :datetime not null +# account_id :integer not null +# author_id :bigint +# category_id :integer +# folder_id :integer +# portal_id :integer not null +# +# Indexes +# +# index_articles_on_author_id (author_id) +# +# Foreign Keys +# +# fk_rails_... (author_id => users.id) # class Article < ApplicationRecord + include PgSearch::Model + belongs_to :account belongs_to :category belongs_to :portal - belongs_to :folder belongs_to :author, class_name: 'User' before_validation :ensure_account_id @@ -32,6 +41,36 @@ class Article < ApplicationRecord enum status: { draft: 0, published: 1 } + scope :search_by_category_slug, ->(category_slug) { where(categories: { slug: category_slug }) if category_slug.present? } + scope :search_by_category_locale, ->(locale) { where(categories: { locale: locale }) if locale.present? } + + # TODO: if text search slows down https://www.postgresql.org/docs/current/textsearch-features.html#TEXTSEARCH-UPDATE-TRIGGERS + pg_search_scope( + :text_search, + against: %i[ + title + description + content + ], + using: { + tsearch: { + prefix: true + } + } + ) + + def self.search(params) + records = joins( + :category + ).search_by_category_slug(params[:category_slug]).search_by_category_locale(params[:locale]) + records.text_search(params[:query]) if params[:query].present? + records.page(current_page(params)) + end + + def self.current_page(params) + params[:page] || 1 + end + private def ensure_account_id diff --git a/app/models/category.rb b/app/models/category.rb index 2fb5815f2..8c5989c0f 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -2,20 +2,22 @@ # # Table name: categories # -# id :bigint not null, primary key -# description :text -# locale :string default("en") -# name :string -# position :integer -# created_at :datetime not null -# updated_at :datetime not null -# account_id :integer not null -# portal_id :integer not null +# id :bigint not null, primary key +# description :text +# locale :string default("en") +# name :string +# position :integer +# slug :string not null +# created_at :datetime not null +# updated_at :datetime not null +# account_id :integer not null +# portal_id :integer not null # # Indexes # -# index_categories_on_locale (locale) -# index_categories_on_locale_and_account_id (locale,account_id) +# index_categories_on_locale (locale) +# index_categories_on_locale_and_account_id (locale,account_id) +# index_categories_on_slug_and_locale_and_portal_id (slug,locale,portal_id) UNIQUE # class Category < ApplicationRecord belongs_to :account @@ -25,7 +27,20 @@ class Category < ApplicationRecord before_validation :ensure_account_id validates :account_id, presence: true + validates :slug, presence: true validates :name, presence: true + validates :locale, uniqueness: { scope: %i[slug portal_id], + message: 'should be unique in the category and portal' } + + scope :search_by_locale, ->(locale) { where(locale: locale) if locale.present? } + + def self.search(params) + search_by_locale(params[:locale]).page(current_page(params)).order(position: :asc) + end + + def self.current_page(params) + params[:page] || 1 + end private diff --git a/app/models/user.rb b/app/models/user.rb index 534ebea21..2125f88d2 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -92,6 +92,7 @@ class User < ApplicationRecord has_many :portals, through: :portals_members has_many :team_members, dependent: :destroy_async has_many :teams, through: :team_members + has_many :articles, foreign_key: 'author_id', dependent: :nullify before_validation :set_password_and_uid, on: :create diff --git a/app/views/api/v1/accounts/articles/_article.json.jbuilder b/app/views/api/v1/accounts/articles/_article.json.jbuilder new file mode 100644 index 000000000..85ff7c1a0 --- /dev/null +++ b/app/views/api/v1/accounts/articles/_article.json.jbuilder @@ -0,0 +1,20 @@ +json.id article.id +json.category_id article.category_id +json.title article.title +json.content article.content +json.description article.description +json.status article.status +json.account_id article.account_id + +if article.portal.present? + json.portal do + json.partial! 'api/v1/accounts/portals/portal.json.jbuilder', portal: article.portal + end +end +json.views article.views + +if article.author.present? + json.author do + json.partial! 'api/v1/models/agent.json.jbuilder', resource: article.author + end +end diff --git a/app/views/api/v1/accounts/articles/create.json.jbuilder b/app/views/api/v1/accounts/articles/create.json.jbuilder new file mode 100644 index 000000000..fdc6247c2 --- /dev/null +++ b/app/views/api/v1/accounts/articles/create.json.jbuilder @@ -0,0 +1,3 @@ +json.payload do + json.partial! 'article', article: @article +end diff --git a/app/views/api/v1/accounts/articles/edit.json.jbuilder b/app/views/api/v1/accounts/articles/edit.json.jbuilder new file mode 100644 index 000000000..fdc6247c2 --- /dev/null +++ b/app/views/api/v1/accounts/articles/edit.json.jbuilder @@ -0,0 +1,3 @@ +json.payload do + json.partial! 'article', article: @article +end diff --git a/app/views/api/v1/accounts/articles/index.json.jbuilder b/app/views/api/v1/accounts/articles/index.json.jbuilder new file mode 100644 index 000000000..3e3364007 --- /dev/null +++ b/app/views/api/v1/accounts/articles/index.json.jbuilder @@ -0,0 +1,3 @@ +json.payload do + json.array! @articles, partial: 'article', as: :article +end diff --git a/app/views/api/v1/accounts/articles/show.json.jbuilder b/app/views/api/v1/accounts/articles/show.json.jbuilder new file mode 100644 index 000000000..fdc6247c2 --- /dev/null +++ b/app/views/api/v1/accounts/articles/show.json.jbuilder @@ -0,0 +1,3 @@ +json.payload do + json.partial! 'article', article: @article +end diff --git a/app/views/api/v1/accounts/articles/update.json.jbuilder b/app/views/api/v1/accounts/articles/update.json.jbuilder new file mode 100644 index 000000000..fdc6247c2 --- /dev/null +++ b/app/views/api/v1/accounts/articles/update.json.jbuilder @@ -0,0 +1,3 @@ +json.payload do + json.partial! 'article', article: @article +end diff --git a/app/views/api/v1/accounts/categories/_category.json.jbuilder b/app/views/api/v1/accounts/categories/_category.json.jbuilder index 358420231..4b4915da9 100644 --- a/app/views/api/v1/accounts/categories/_category.json.jbuilder +++ b/app/views/api/v1/accounts/categories/_category.json.jbuilder @@ -1,5 +1,7 @@ json.id category.id json.name category.name +json.slug category.slug +json.locale category.locale json.description category.description json.position category.position json.account_id category.account_id diff --git a/config/routes.rb b/config/routes.rb index a09311b3e..5d1718841 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -162,8 +162,8 @@ Rails.application.routes.draw do resources :categories do resources :folders end + resources :articles end - resources :articles end end # end of account scoped api routes diff --git a/db/migrate/20220527080906_add_reference_for_author_id.rb b/db/migrate/20220527080906_add_reference_for_author_id.rb new file mode 100644 index 000000000..cf7fd22fc --- /dev/null +++ b/db/migrate/20220527080906_add_reference_for_author_id.rb @@ -0,0 +1,6 @@ +class AddReferenceForAuthorId < ActiveRecord::Migration[6.1] + def change + remove_column :articles, :author_id, :integer + add_reference :articles, :author, foreign_key: { to_table: :users } + end +end diff --git a/db/migrate/20220527120826_add_index_on_category_slug_and_locale.rb b/db/migrate/20220527120826_add_index_on_category_slug_and_locale.rb new file mode 100644 index 000000000..be17caac7 --- /dev/null +++ b/db/migrate/20220527120826_add_index_on_category_slug_and_locale.rb @@ -0,0 +1,7 @@ +class AddIndexOnCategorySlugAndLocale < ActiveRecord::Migration[6.1] + def change + add_column :categories, :slug, :string, null: false, default: '' + add_index :categories, [:slug, :locale, :portal_id], unique: true + change_column_default :categories, :slug, from: '', to: nil + end +end diff --git a/db/schema.rb b/db/schema.rb index 9599d193c..6f1ffe408 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -116,7 +116,6 @@ ActiveRecord::Schema.define(version: 2022_06_10_091206) do t.integer "portal_id", null: false t.integer "category_id" t.integer "folder_id" - t.integer "author_id" t.string "title" t.text "description" t.text "content" @@ -124,6 +123,8 @@ ActiveRecord::Schema.define(version: 2022_06_10_091206) do t.integer "views" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false + t.bigint "author_id" + t.index ["author_id"], name: "index_articles_on_author_id" end create_table "attachments", id: :serial, force: :cascade do |t| @@ -193,8 +194,10 @@ ActiveRecord::Schema.define(version: 2022_06_10_091206) do t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.string "locale", default: "en" + t.string "slug", null: false t.index ["locale", "account_id"], name: "index_categories_on_locale_and_account_id" t.index ["locale"], name: "index_categories_on_locale" + t.index ["slug", "locale", "portal_id"], name: "index_categories_on_slug_and_locale_and_portal_id", unique: true end create_table "channel_api", force: :cascade do |t| @@ -817,6 +820,7 @@ ActiveRecord::Schema.define(version: 2022_06_10_091206) do add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "agent_bots", "accounts", on_delete: :cascade + add_foreign_key "articles", "users", column: "author_id" add_foreign_key "campaigns", "accounts", on_delete: :cascade add_foreign_key "campaigns", "inboxes", on_delete: :cascade add_foreign_key "contact_inboxes", "contacts", on_delete: :cascade diff --git a/spec/controllers/api/v1/accounts/articles_controller_spec.rb b/spec/controllers/api/v1/accounts/articles_controller_spec.rb new file mode 100644 index 000000000..69c1ee4ca --- /dev/null +++ b/spec/controllers/api/v1/accounts/articles_controller_spec.rb @@ -0,0 +1,137 @@ +require 'rails_helper' + +RSpec.describe 'Api::V1::Accounts::Articles', type: :request do + let(:account) { create(:account) } + let(:agent) { create(:user, account: account, role: :agent) } + let!(:portal) { create(:portal, name: 'test_portal', account_id: account.id) } + let!(:category) { create(:category, name: 'category', portal: portal, account_id: account.id, locale: 'en', slug: 'category_slug') } + let!(:article) { create(:article, category: category, portal: portal, account_id: account.id, author_id: agent.id) } + + describe 'POST /api/v1/accounts/{account.id}/portals/{portal.slug}/articles' do + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles", params: {} + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + it 'creates article' do + article_params = { + article: { + category_id: category.id, + description: 'test description', + title: 'MyTitle', + content: 'This is my content.', + status: :published, + author_id: agent.id + } + } + post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles", + params: article_params, + headers: agent.create_new_auth_token + expect(response).to have_http_status(:success) + json_response = JSON.parse(response.body) + expect(json_response['payload']['title']).to eql('MyTitle') + end + end + end + + describe 'PUT /api/v1/accounts/{account.id}/portals/{portal.slug}/articles/{article.id}' do + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles/#{article.id}", params: {} + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + it 'updates category' do + article_params = { + article: { + title: 'MyTitle2', + description: 'test_description' + } + } + + expect(article.title).not_to eql(article_params[:article][:title]) + + put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles/#{article.id}", + params: article_params, + headers: agent.create_new_auth_token + expect(response).to have_http_status(:success) + json_response = JSON.parse(response.body) + expect(json_response['payload']['title']).to eql(article_params[:article][:title]) + end + end + end + + describe 'DELETE /api/v1/accounts/{account.id}/portals/{portal.slug}/articles/{article.id}' do + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + delete "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles/#{article.id}", params: {} + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + it 'deletes category' do + delete "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles/#{article.id}", + headers: agent.create_new_auth_token + expect(response).to have_http_status(:success) + deleted_article = Article.find_by(id: article.id) + expect(deleted_article).to be nil + end + end + end + + describe 'GET /api/v1/accounts/{account.id}/portals/{portal.slug}/articles' do + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + get "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles" + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + it 'get all articles' do + article2 = create(:article, account_id: account.id, portal: portal, category: category, author_id: agent.id) + expect(article2.id).not_to be nil + + get "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles", + headers: agent.create_new_auth_token, + params: { payload: {} } + expect(response).to have_http_status(:success) + json_response = JSON.parse(response.body) + expect(json_response['payload'].count).to be 2 + end + + it 'get all articles with searched params' do + article2 = create(:article, account_id: account.id, portal: portal, category: category, author_id: agent.id) + expect(article2.id).not_to be nil + + get "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles", + headers: agent.create_new_auth_token, + params: { payload: { category_slug: category.slug } } + expect(response).to have_http_status(:success) + json_response = JSON.parse(response.body) + expect(json_response['payload'].count).to be 2 + end + end + + describe 'GET /api/v1/accounts/{account.id}/portals/{portal.slug}/articles/{article.id}' do + it 'get article' do + article2 = create(:article, account_id: account.id, portal: portal, category: category, author_id: agent.id) + expect(article2.id).not_to be nil + + get "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles/#{article2.id}", + headers: agent.create_new_auth_token + expect(response).to have_http_status(:success) + json_response = JSON.parse(response.body) + + expect(json_response['payload']['title']).to eq(article2.title) + expect(json_response['payload']['id']).to eq(article2.id) + end + end + end +end diff --git a/spec/controllers/api/v1/accounts/categories_controller_spec.rb b/spec/controllers/api/v1/accounts/categories_controller_spec.rb index a1b767166..053fe9d5e 100644 --- a/spec/controllers/api/v1/accounts/categories_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/categories_controller_spec.rb @@ -4,7 +4,7 @@ RSpec.describe 'Api::V1::Accounts::Categories', type: :request do let(:account) { create(:account) } let(:agent) { create(:user, account: account, role: :agent) } let!(:portal) { create(:portal, name: 'test_portal', account_id: account.id) } - let!(:category) { create(:category, name: 'category', portal: portal, account_id: account.id) } + let!(:category) { create(:category, name: 'category', portal: portal, account_id: account.id, slug: 'category_slug') } describe 'POST /api/v1/accounts/{account.id}/portals/{portal.slug}/categories' do context 'when it is an unauthenticated user' do @@ -15,14 +15,17 @@ RSpec.describe 'Api::V1::Accounts::Categories', type: :request do end context 'when it is an authenticated user' do - it 'creates category' do - category_params = { - category: { - name: 'test_category', - description: 'test_description', - position: 1 - } + category_params = { + category: { + name: 'test_category', + description: 'test_description', + position: 1, + locale: 'es', + slug: 'test_category_1' } + } + + it 'creates category' do post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories", params: category_params, headers: agent.create_new_auth_token @@ -30,6 +33,37 @@ RSpec.describe 'Api::V1::Accounts::Categories', type: :request do json_response = JSON.parse(response.body) expect(json_response['payload']['name']).to eql('test_category') end + + it 'will throw an error on locale, category_id uniqueness' do + post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories", + params: category_params, + headers: agent.create_new_auth_token + + post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories", + params: category_params, + headers: agent.create_new_auth_token + expect(response).to have_http_status(:unprocessable_entity) + json_response = JSON.parse(response.body) + expect(json_response['message']).to eql('Locale should be unique in the category and portal') + end + + it 'will throw an error slug presence' do + category_params = { + category: { + name: 'test_category', + description: 'test_description', + position: 1, + locale: 'es' + } + } + + post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories", + params: category_params, + headers: agent.create_new_auth_token + expect(response).to have_http_status(:unprocessable_entity) + json_response = JSON.parse(response.body) + expect(json_response['message']).to eql("Slug can't be blank") + end end end @@ -92,7 +126,7 @@ RSpec.describe 'Api::V1::Accounts::Categories', type: :request do context 'when it is an authenticated user' do it 'get all portals' do - category2 = create(:category, name: 'test_category_2', portal: portal) + category2 = create(:category, name: 'test_category_2', portal: portal, locale: 'es', slug: 'category_slug_2') expect(category2.id).not_to be nil get "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories", diff --git a/spec/factories/articles.rb b/spec/factories/articles.rb index 3f0c94c47..9ba5d9ac4 100644 --- a/spec/factories/articles.rb +++ b/spec/factories/articles.rb @@ -2,13 +2,11 @@ FactoryBot.define do factory :article, class: 'Article' do account_id { 1 } category_id { 1 } - folder_id { 1 } author_id { 1 } title { 'MyString' } content { 'MyText' } + description { 'MyDescrption' } status { 1 } views { 1 } - seo_title { 'MyString' } - seo { '' } end end diff --git a/spec/models/article_spec.rb b/spec/models/article_spec.rb index ee1e7f618..1945cca62 100644 --- a/spec/models/article_spec.rb +++ b/spec/models/article_spec.rb @@ -14,4 +14,105 @@ RSpec.describe Article, type: :model do it { is_expected.to belong_to(:category) } it { is_expected.to belong_to(:author) } end + + describe 'search' do + let!(:account) { create(:account) } + let(:user) { create(:user, account_ids: [account.id], role: :agent) } + let!(:portal_1) { create(:portal, account_id: account.id) } + let!(:portal_2) { create(:portal, account_id: account.id) } + let!(:category_1) { create(:category, slug: 'category_1', locale: 'en', portal_id: portal_1.id) } + let!(:category_2) { create(:category, slug: 'category_2', locale: 'es', portal_id: portal_1.id) } + let!(:category_3) { create(:category, slug: 'category_3', locale: 'es', portal_id: portal_2.id) } + + before do + create(:article, category_id: category_1.id, content: 'This is the content', description: 'this is the description', title: 'this is title', + portal_id: portal_1.id, author_id: user.id) + create(:article, category_id: category_1.id, title: 'title 1', portal_id: portal_1.id, author_id: user.id) + create(:article, category_id: category_2.id, title: 'title 2', portal_id: portal_2.id, author_id: user.id) + create(:article, category_id: category_2.id, title: 'title 3', portal_id: portal_1.id, author_id: user.id) + create(:article, category_id: category_3.id, title: 'title 6', portal_id: portal_2.id, author_id: user.id) + create(:article, category_id: category_2.id, title: 'title 7', portal_id: portal_1.id, author_id: user.id) + end + + context 'when no parameters passed' do + it 'returns all the articles in portal' do + records = portal_1.articles.search({}) + expect(records.count).to eq(portal_1.articles.count) + + records = portal_2.articles.search({}) + expect(records.count).to eq(portal_2.articles.count) + end + end + + context 'when params passed' do + it 'returns all the articles with all the params filters' do + params = { query: 'title', locale: 'es', category_slug: 'category_3' } + records = portal_2.articles.search(params) + expect(records.count).to eq(1) + + params = { query: 'this', locale: 'en', category_slug: 'category_1' } + records = portal_1.articles.search(params) + expect(records.count).to eq(2) + end + end + + context 'when some params missing' do + it 'returns data with category slug' do + params = { category_slug: 'category_2' } + records = portal_1.articles.search(params) + expect(records.count).to eq(2) + end + + it 'returns data with locale' do + params = { locale: 'es' } + records = portal_2.articles.search(params) + expect(records.count).to eq(2) + + params = { locale: 'en' } + records = portal_1.articles.search(params) + expect(records.count).to eq(2) + end + + it 'returns data with text_search query' do + params = { query: 'title' } + records = portal_2.articles.search(params) + expect(records.count).to eq(2) + + params = { query: 'title' } + records = portal_1.articles.search(params) + expect(records.count).to eq(4) + + params = { query: 'the content' } + records = portal_2.articles.search(params) + expect(records.count).to eq(2) + end + + it 'returns data with text_search query and locale' do + params = { query: 'the title', locale: 'es' } + records = portal_2.articles.search(params) + expect(records.count).to eq(2) + end + + it 'returns records with locale and category_slug' do + params = { category_slug: 'category_2', locale: 'es' } + records = portal_1.articles.search(params) + expect(records.count).to eq(2) + end + + it 'return records with category_slug and text_search query' do + params = { category_slug: 'category_2', query: 'the title' } + records = portal_1.articles.search(params) + expect(records.count).to eq(2) + end + end + + context 'with pagination' do + it 'returns paginated articles' do + create_list(:article, 30, category_id: category_2.id, title: 'title 1', portal_id: portal_2.id, author_id: user.id) + params = { category_slug: 'category_2' } + records = portal_2.articles.search(params) + expect(records.count).to eq(25) + end + end + end end diff --git a/spec/models/category_spec.rb b/spec/models/category_spec.rb index 55bad9a14..37b17284a 100644 --- a/spec/models/category_spec.rb +++ b/spec/models/category_spec.rb @@ -12,4 +12,39 @@ RSpec.describe Category, type: :model do it { is_expected.to have_many(:folders) } it { is_expected.to have_many(:articles) } end + + describe 'search' do + let!(:account) { create(:account) } + let(:user) { create(:user, account_ids: [account.id], role: :agent) } + let!(:portal_1) { create(:portal, account_id: account.id) } + let!(:portal_2) { create(:portal, account_id: account.id) } + + before do + create(:category, slug: 'category_1', locale: 'en', portal_id: portal_1.id) + create(:category, slug: 'category_2', locale: 'es', portal_id: portal_1.id) + create(:category, slug: 'category_3', locale: 'es', portal_id: portal_2.id) + end + + context 'when no parameters passed' do + it 'returns all the articles in portal' do + records = portal_1.categories.search({}) + expect(records.count).to eq(portal_1.categories.count) + + records = portal_2.categories.search({}) + expect(records.count).to eq(portal_2.categories.count) + end + end + + context 'when params passed' do + it 'returns all the categories with all the params filters' do + params = { locale: 'es' } + records = portal_2.categories.search(params) + expect(records.count).to eq(1) + + params = { locale: 'en' } + records = portal_1.categories.search(params) + expect(records.count).to eq(1) + end + end + end end From 713fdb44ee09e7f16ffb83d71c5faf7036d6d807 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Mon, 13 Jun 2022 20:18:38 +0530 Subject: [PATCH 163/177] feat (ee): APIs to configure an auto assignment limit for inboxes (#4672) Co-authored-by: Pranav Raj S --- .../api/v1/accounts/inboxes_controller.rb | 30 +++------- app/controllers/application_controller.rb | 2 +- app/controllers/dashboard_controller.rb | 3 +- app/helpers/api/v1/inboxes_helper.rb | 18 ++++++ .../dashboard/i18n/locale/en/inboxMgmt.json | 5 ++ .../inbox/settingsPage/CollaboratorsPage.vue | 56 ++++++++++++++++++- app/javascript/shared/mixins/configMixin.js | 3 + app/models/account.rb | 3 +- app/models/concerns/round_robin_handler.rb | 2 +- app/models/inbox.rb | 12 ++++ app/services/round_robin/manage_service.rb | 33 ++++++----- app/views/api/v1/models/_inbox.json.jbuilder | 1 + app/views/layouts/vueapp.html.erb | 1 + ...uto_assignment_configuration_to_inboxes.rb | 5 ++ db/schema.rb | 1 + .../api/v1/accounts/inboxes_controller.rb | 9 +++ enterprise/app/models/enterprise/inbox.rb | 20 +++++++ .../v1/accounts/inboxes_controller_spec.rb | 46 +++++++++++++++ spec/enterprise/models/inbox_spec.rb | 38 +++++++++++++ .../round_robin/manage_service_spec.rb | 12 ++-- 20 files changed, 255 insertions(+), 45 deletions(-) create mode 100644 db/migrate/20220527040433_add_auto_assignment_configuration_to_inboxes.rb create mode 100644 enterprise/app/controllers/enterprise/api/v1/accounts/inboxes_controller.rb create mode 100644 enterprise/app/models/enterprise/inbox.rb create mode 100644 spec/enterprise/controllers/api/v1/accounts/inboxes_controller_spec.rb create mode 100644 spec/enterprise/models/inbox_spec.rb diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 4bc85546a..ded4db598 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -42,7 +42,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController end def update - @inbox.update(permitted_params.except(:channel)) + @inbox.update!(permitted_params.except(:channel)) @inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours] channel_attributes = get_channel_attributes(@inbox.channel_type) @@ -109,10 +109,14 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController @inbox.channel.save! end + def inbox_attributes + [:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled, + :enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved] + end + 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, :allow_messages_after_resolved, + *inbox_attributes, channel: [:type, *channel_attributes] ) end @@ -129,18 +133,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController }[permitted_params[:channel][:type]] end - def account_channels_method - { - 'web_widget' => Current.account.web_widgets, - 'api' => Current.account.api_channels, - 'email' => Current.account.email_channels, - 'line' => Current.account.line_channels, - 'telegram' => Current.account.telegram_channels, - 'whatsapp' => Current.account.whatsapp_channels, - 'sms' => Current.account.sms_channels - }[permitted_params[:channel][:type]] - end - def get_channel_attributes(channel_type) if channel_type.constantize.const_defined?(:EDITABLE_ATTRS) channel_type.constantize::EDITABLE_ATTRS.presence @@ -148,10 +140,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController [] end end - - def validate_limit - return unless Current.account.inboxes.count >= Current.account.usage_limits[:inboxes] - - render_payment_required('Account limit exceeded. Upgrade to a higher plan') - end end + +Api::V1::Accounts::InboxesController.prepend_mod_with('Api::V1::Accounts::InboxesController') diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0a1c672ec..0762c3d91 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,7 +1,7 @@ class ApplicationController < ActionController::Base include DeviseTokenAuth::Concerns::SetUserByToken include RequestExceptionHandler - include Pundit + include Pundit::Authorization include SwitchLocale skip_before_action :verify_authenticity_token diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 243fe00b2..36c789b52 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -43,7 +43,8 @@ class DashboardController < ActionController::Base VAPID_PUBLIC_KEY: VapidService.public_key, ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'), FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''), - FACEBOOK_API_VERSION: 'v14.0' + FACEBOOK_API_VERSION: 'v14.0', + IS_ENTERPRISE: ChatwootApp.enterprise? } end end diff --git a/app/helpers/api/v1/inboxes_helper.rb b/app/helpers/api/v1/inboxes_helper.rb index 56fb79908..82644e62f 100644 --- a/app/helpers/api/v1/inboxes_helper.rb +++ b/app/helpers/api/v1/inboxes_helper.rb @@ -74,4 +74,22 @@ module Api::V1::InboxesHelper context.verify_mode = openssl_verify_mode context end + + def account_channels_method + { + 'web_widget' => Current.account.web_widgets, + 'api' => Current.account.api_channels, + 'email' => Current.account.email_channels, + 'line' => Current.account.line_channels, + 'telegram' => Current.account.telegram_channels, + 'whatsapp' => Current.account.whatsapp_channels, + 'sms' => Current.account.sms_channels + }[permitted_params[:channel][:type]] + end + + def validate_limit + return unless Current.account.inboxes.count >= Current.account.usage_limits[:inboxes] + + render_payment_required('Account limit exceeded. Upgrade to a higher plan') + end end diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index 252a041bc..6bc5603b0 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -421,6 +421,11 @@ "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." }, + "AUTO_ASSIGNMENT":{ + "MAX_ASSIGNMENT_LIMIT": "Auto assignment limit", + "MAX_ASSIGNMENT_LIMIT_RANGE_ERROR": "Please enter a value greater than 0", + "MAX_ASSIGNMENT_LIMIT_SUB_TEXT": "Limit the maximum number of conversations from this inbox that can be auto assigned to an agent" + }, "FACEBOOK_REAUTHORIZE": { "TITLE": "Reauthorize", "SUBTITLE": "Your Facebook connection has expired, please reconnect your Facebook page to continue services", diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/CollaboratorsPage.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/CollaboratorsPage.vue index 2b3fa413f..68f5bc5e3 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/CollaboratorsPage.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/CollaboratorsPage.vue @@ -48,20 +48,47 @@ {{ $t('INBOX_MGMT.SETTINGS_POPUP.AUTO_ASSIGNMENT_SUB_TEXT') }}

+ + +
+ + +

+ {{ $t('INBOX_MGMT.AUTO_ASSIGNMENT.MAX_ASSIGNMENT_LIMIT_SUB_TEXT') }} +

+ + +
+ + diff --git a/app/javascript/shared/mixins/configMixin.js b/app/javascript/shared/mixins/configMixin.js index acbe98511..7561f08df 100644 --- a/app/javascript/shared/mixins/configMixin.js +++ b/app/javascript/shared/mixins/configMixin.js @@ -9,5 +9,8 @@ export default { enabledLanguages() { return window.chatwootConfig.enabledLanguages; }, + isEnterprise() { + return window.chatwootConfig.isEnterprise === 'true'; + }, }, }; diff --git a/app/models/account.rb b/app/models/account.rb index 7dd1cc229..62af85e86 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -20,7 +20,6 @@ class Account < ApplicationRecord include FlagShihTzu include Reportable include Featurable - prepend_mod_with('Account') DEFAULT_QUERY_SETTING = { flag_query_mode: :bit_operator, @@ -146,3 +145,5 @@ class Account < ApplicationRecord ActiveRecord::Base.connection.exec_query("drop sequence IF EXISTS conv_dpid_seq_#{id}") end end + +Account.prepend_mod_with('Account') diff --git a/app/models/concerns/round_robin_handler.rb b/app/models/concerns/round_robin_handler.rb index 80865acd7..f543f8e65 100644 --- a/app/models/concerns/round_robin_handler.rb +++ b/app/models/concerns/round_robin_handler.rb @@ -14,7 +14,7 @@ module RoundRobinHandler return unless conversation_status_changed_to_open? return unless should_round_robin? - ::RoundRobin::AssignmentService.new(conversation: self).perform + ::RoundRobin::AssignmentService.new(conversation: self, allowed_member_ids: inbox.member_ids_with_assignment_capacity).perform end def should_round_robin? diff --git a/app/models/inbox.rb b/app/models/inbox.rb index 2801fa834..b58e6870a 100644 --- a/app/models/inbox.rb +++ b/app/models/inbox.rb @@ -6,6 +6,7 @@ # # id :integer not null, primary key # allow_messages_after_resolved :boolean default(TRUE) +# auto_assignment_config :jsonb # channel_type :string # csat_survey_enabled :boolean default(FALSE) # email_address :string @@ -35,6 +36,7 @@ class Inbox < ApplicationRecord validates :name, presence: true validates :account_id, presence: true validates :timezone, inclusion: { in: TZInfo::Timezone.all_identifiers } + validate :ensure_valid_max_assignment_limit belongs_to :account @@ -118,9 +120,19 @@ class Inbox < ApplicationRecord end end + def member_ids_with_assignment_capacity + members.ids + end + private + def ensure_valid_max_assignment_limit + # overridden in enterprise/app/models/enterprise/inbox.rb + end + def delete_round_robin_agents ::RoundRobin::ManageService.new(inbox: self).clear_queue end end + +Inbox.prepend_mod_with('Inbox') diff --git a/app/services/round_robin/manage_service.rb b/app/services/round_robin/manage_service.rb index 9b3a60b53..3b5fd1660 100644 --- a/app/services/round_robin/manage_service.rb +++ b/app/services/round_robin/manage_service.rb @@ -1,3 +1,6 @@ +# NOTE: available agent method now expect allowed_member_ids to be passed in always because of inbox limits feature +# need to refactor this class and split the queue managment into a seperate class + # If allowed_member_ids are supplied round robin service will only fetch a member from member id # This is used in case of team assignment class RoundRobin::ManageService @@ -18,6 +21,13 @@ class RoundRobin::ManageService ::Redis::Alfred.lrem(round_robin_key, user_id) end + def reset_queue + clear_queue + add_agent_to_queue(inbox.inbox_members.map(&:user_id)) + end + + # end of queue management functions + def available_agent(priority_list: []) reset_queue unless validate_queue? user_id = get_member_via_priority_list(priority_list) @@ -26,29 +36,22 @@ class RoundRobin::ManageService inbox.inbox_members.find_by(user_id: user_id)&.user if user_id.present? end - def reset_queue - clear_queue - add_agent_to_queue(inbox.inbox_members.map(&:user_id)) - end - private def fetch_user_id - if allowed_member_ids_in_str.present? - user_id = queue.intersection(allowed_member_ids_in_str).pop - pop_push_to_queue(user_id) - user_id - else - ::Redis::Alfred.rpoplpush(round_robin_key, round_robin_key) - end + return nil if allowed_member_ids_in_str.blank? + + user_id = queue.intersection(allowed_member_ids_in_str).pop + pop_push_to_queue(user_id) + user_id end - # priority list is usually the members who are online passed from assignmebt service + # priority list is usually the members who are online passed from assignment service def get_member_via_priority_list(priority_list) return if priority_list.blank? - # when allowed member ids is passed we will be looking to get members from that list alone - priority_list = priority_list.intersection(allowed_member_ids_in_str) if allowed_member_ids_in_str.present? + # When allowed member ids is passed we will be looking to get members from that list alone + priority_list = priority_list.intersection(allowed_member_ids_in_str) return if priority_list.blank? user_id = queue.intersection(priority_list.map(&:to_s)).pop diff --git a/app/views/api/v1/models/_inbox.json.jbuilder b/app/views/api/v1/models/_inbox.json.jbuilder index 18b737da3..b91b04549 100644 --- a/app/views/api/v1/models/_inbox.json.jbuilder +++ b/app/views/api/v1/models/_inbox.json.jbuilder @@ -9,6 +9,7 @@ json.working_hours_enabled resource.working_hours_enabled json.enable_email_collect resource.enable_email_collect json.csat_survey_enabled resource.csat_survey_enabled json.enable_auto_assignment resource.enable_auto_assignment +json.auto_assignment_config resource.auto_assignment_config json.out_of_office_message resource.out_of_office_message json.working_hours resource.weekly_schedule json.timezone resource.timezone diff --git a/app/views/layouts/vueapp.html.erb b/app/views/layouts/vueapp.html.erb index 3f91ac042..ae7393ee6 100644 --- a/app/views/layouts/vueapp.html.erb +++ b/app/views/layouts/vueapp.html.erb @@ -36,6 +36,7 @@ fbAppId: '<%= ENV.fetch('FB_APP_ID', nil) %>', fbApiVersion: '<%= @global_config['FACEBOOK_API_VERSION'] %>', signupEnabled: '<%= @global_config['ENABLE_ACCOUNT_SIGNUP'] %>', + isEnterprise: '<%= @global_config['IS_ENTERPRISE'] %>', <% if @global_config['VAPID_PUBLIC_KEY'] %> vapidPublicKey: new Uint8Array(<%= Base64.urlsafe_decode64(@global_config['VAPID_PUBLIC_KEY']).bytes %>), <% end %> diff --git a/db/migrate/20220527040433_add_auto_assignment_configuration_to_inboxes.rb b/db/migrate/20220527040433_add_auto_assignment_configuration_to_inboxes.rb new file mode 100644 index 000000000..31e324ce9 --- /dev/null +++ b/db/migrate/20220527040433_add_auto_assignment_configuration_to_inboxes.rb @@ -0,0 +1,5 @@ +class AddAutoAssignmentConfigurationToInboxes < ActiveRecord::Migration[6.1] + def change + add_column :inboxes, :auto_assignment_config, :jsonb, default: {} + end +end diff --git a/db/schema.rb b/db/schema.rb index 6f1ffe408..6ab2179d3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -513,6 +513,7 @@ ActiveRecord::Schema.define(version: 2022_06_10_091206) do t.boolean "enable_email_collect", default: true t.boolean "csat_survey_enabled", default: false t.boolean "allow_messages_after_resolved", default: true + t.jsonb "auto_assignment_config", default: {} t.index ["account_id"], name: "index_inboxes_on_account_id" end diff --git a/enterprise/app/controllers/enterprise/api/v1/accounts/inboxes_controller.rb b/enterprise/app/controllers/enterprise/api/v1/accounts/inboxes_controller.rb new file mode 100644 index 000000000..b39db609d --- /dev/null +++ b/enterprise/app/controllers/enterprise/api/v1/accounts/inboxes_controller.rb @@ -0,0 +1,9 @@ +module Enterprise::Api::V1::Accounts::InboxesController + def inbox_attributes + super + ee_inbox_attributes + end + + def ee_inbox_attributes + [auto_assignment_config: [:max_assignment_limit]] + end +end diff --git a/enterprise/app/models/enterprise/inbox.rb b/enterprise/app/models/enterprise/inbox.rb new file mode 100644 index 000000000..6fe3ff2f2 --- /dev/null +++ b/enterprise/app/models/enterprise/inbox.rb @@ -0,0 +1,20 @@ +module Enterprise::Inbox + def member_ids_with_assignment_capacity + max_assignment_limit = auto_assignment_config['max_assignment_limit'] + overloaded_agent_ids = max_assignment_limit.present? ? get_agent_ids_over_assignment_limit(max_assignment_limit) : [] + super - overloaded_agent_ids + end + + private + + def get_agent_ids_over_assignment_limit(limit) + conversations.open.select(:assignee_id).group(:assignee_id).having("count(*) >= #{limit.to_i}").filter_map(&:assignee_id) + end + + def ensure_valid_max_assignment_limit + return if auto_assignment_config['max_assignment_limit'].blank? + return if auto_assignment_config['max_assignment_limit'].to_i.positive? + + errors.add(:auto_assignment_config, 'max_assignment_limit must be greater than 0') + end +end diff --git a/spec/enterprise/controllers/api/v1/accounts/inboxes_controller_spec.rb b/spec/enterprise/controllers/api/v1/accounts/inboxes_controller_spec.rb new file mode 100644 index 000000000..724a7b0cb --- /dev/null +++ b/spec/enterprise/controllers/api/v1/accounts/inboxes_controller_spec.rb @@ -0,0 +1,46 @@ +require 'rails_helper' + +RSpec.describe 'Enterprise Inboxes API', type: :request do + let(:account) { create(:account) } + let(:admin) { create(:user, account: account, role: :administrator) } + + describe 'POST /api/v1/accounts/{account.id}/inboxes' do + let(:inbox) { create(:inbox, account: account) } + + context 'when it is an authenticated user' do + let(:admin) { create(:user, account: account, role: :administrator) } + let(:valid_params) do + { name: 'test', auto_assignment_config: { max_assignment_limit: 10 }, channel: { type: 'web_widget', website_url: 'test.com' } } + end + + it 'creates a webwidget inbox with auto assignment config' 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) + expect(JSON.parse(response.body)['auto_assignment_config']['max_assignment_limit']).to eq 10 + end + end + end + + describe 'PATCH /api/v1/accounts/{account.id}/inboxes/:id' do + let(:inbox) { create(:inbox, account: account, auto_assignment_config: { max_assignment_limit: 5 }) } + + context 'when it is an authenticated user' do + let(:admin) { create(:user, account: account, role: :administrator) } + let(:valid_params) { { name: 'new test inbox', auto_assignment_config: { max_assignment_limit: 10 } } } + + it 'updates inbox with auto assignment config' do + patch "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}", + headers: admin.create_new_auth_token, + params: valid_params, + as: :json + + expect(response).to have_http_status(:success) + expect(JSON.parse(response.body)['auto_assignment_config']['max_assignment_limit']).to eq 10 + end + end + end +end diff --git a/spec/enterprise/models/inbox_spec.rb b/spec/enterprise/models/inbox_spec.rb new file mode 100644 index 000000000..26dbcef16 --- /dev/null +++ b/spec/enterprise/models/inbox_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Inbox do + describe 'member_ids_with_assignment_capacity' do + let!(:inbox) { create(:inbox) } + let!(:inbox_member_1) { create(:inbox_member, inbox: inbox) } + let!(:inbox_member_2) { create(:inbox_member, inbox: inbox) } + let!(:inbox_member_3) { create(:inbox_member, inbox: inbox) } + let!(:inbox_member_4) { create(:inbox_member, inbox: inbox) } + + before do + create(:conversation, inbox: inbox, assignee: inbox_member_1.user) + # to test conversations in other inboxes won't impact + create_list(:conversation, 3, assignee: inbox_member_1.user) + create_list(:conversation, 2, inbox: inbox, assignee: inbox_member_2.user) + create_list(:conversation, 3, inbox: inbox, assignee: inbox_member_3.user) + end + + it 'validated max_assignment_limit' do + account = create(:account) + expect(build(:inbox, account: account, auto_assignment_config: { max_assignment_limit: 0 })).not_to be_valid + expect(build(:inbox, account: account, auto_assignment_config: {})).to be_valid + expect(build(:inbox, account: account, auto_assignment_config: { max_assignment_limit: 1 })).to be_valid + end + + it 'returns member ids with assignment capacity with inbox max_assignment_limit is configured' do + # agent 1 has 1 conversations, agent 2 has 2 conversations, agent 3 has 3 conversations and agent 4 with none + inbox.update(auto_assignment_config: { max_assignment_limit: 2 }) + expect(inbox.member_ids_with_assignment_capacity).to contain_exactly(inbox_member_1.user_id, inbox_member_4.user_id) + end + + it 'returns all member ids when inbox max_assignment_limit is not configured' do + expect(inbox.member_ids_with_assignment_capacity).to eq(inbox.members.ids) + end + end +end diff --git a/spec/services/round_robin/manage_service_spec.rb b/spec/services/round_robin/manage_service_spec.rb index 75110d325..84cca45d1 100644 --- a/spec/services/round_robin/manage_service_spec.rb +++ b/spec/services/round_robin/manage_service_spec.rb @@ -8,10 +8,14 @@ describe RoundRobin::ManageService do let!(:inbox_members) { create_list(:inbox_member, 5, inbox: inbox) } describe '#available_agent' do - it 'gets the first available agent and move agent to end of the list' do + it 'returns nil if allowed_member_ids is empty' do + expect(described_class.new(inbox: inbox, allowed_member_ids: []).available_agent).to eq nil + end + + it 'gets the first available agent in allowed_member_ids and move agent to end of the list' do expected_queue = [inbox_members[0].user_id, inbox_members[4].user_id, inbox_members[3].user_id, inbox_members[2].user_id, inbox_members[1].user_id].map(&:to_s) - round_robin_manage_service.available_agent + described_class.new(inbox: inbox, allowed_member_ids: [inbox_members[0].user_id, inbox_members[4].user_id]).available_agent expect(round_robin_manage_service.send(:queue)).to eq(expected_queue) end @@ -19,8 +23,8 @@ describe RoundRobin::ManageService do expected_queue = [inbox_members[2].user_id, inbox_members[4].user_id, inbox_members[3].user_id, inbox_members[1].user_id, inbox_members[0].user_id].map(&:to_s) # prority list will be ids in string, since thats what redis supplies to us - expect(round_robin_manage_service.available_agent(priority_list: [inbox_members[3].user_id.to_s, - inbox_members[2].user_id.to_s])).to eq inbox_members[2].user + expect(described_class.new(inbox: inbox, allowed_member_ids: [inbox_members[2].user_id]) + .available_agent(priority_list: [inbox_members[3].user_id.to_s, inbox_members[2].user_id.to_s])).to eq inbox_members[2].user expect(round_robin_manage_service.send(:queue)).to eq(expected_queue) end From 37cb46238af99b78ba44093e0e41ff453d496cb9 Mon Sep 17 00:00:00 2001 From: Tejaswini Chile Date: Tue, 14 Jun 2022 10:35:56 +0530 Subject: [PATCH 164/177] chore: Add documentation for automation rule, fix conversation/inbox_members (#4852) --- swagger/definitions/index.yml | 6 + .../automation_rule/create_update_payload.yml | 41 ++ .../definitions/resource/automation_rule.yml | 45 ++ swagger/definitions/resource/contact.yml | 4 + swagger/definitions/resource/conversation.yml | 4 + swagger/index.yml | 1 + .../application/automation_rule/create.yml | 20 + .../application/automation_rule/delete.yml | 20 + .../application/automation_rule/index.yml | 20 + .../application/automation_rule/show.yml | 20 + .../application/automation_rule/update.yml | 27 + .../paths/application/conversation/index.yml | 6 +- .../application/custom_attributes/delete.yml | 1 + .../application/custom_attributes/show.yml | 7 + .../paths/application/team_members/index.yml | 1 + swagger/paths/index.yml | 35 +- swagger/swagger.json | 506 +++++++++++++++--- 17 files changed, 695 insertions(+), 69 deletions(-) create mode 100644 swagger/definitions/request/automation_rule/create_update_payload.yml create mode 100644 swagger/definitions/resource/automation_rule.yml create mode 100644 swagger/paths/application/automation_rule/create.yml create mode 100644 swagger/paths/application/automation_rule/delete.yml create mode 100644 swagger/paths/application/automation_rule/index.yml create mode 100644 swagger/paths/application/automation_rule/show.yml create mode 100644 swagger/paths/application/automation_rule/update.yml diff --git a/swagger/definitions/index.yml b/swagger/definitions/index.yml index 5e22a7099..5e4e50b04 100644 --- a/swagger/definitions/index.yml +++ b/swagger/definitions/index.yml @@ -16,6 +16,8 @@ canned_response: $ref: ./resource/canned_response.yml custom_attribute: $ref: ./resource/custom_attribute.yml +automation_rule: + $ref: ./resource/automation_rule.yml contact: $ref: ./resource/contact.yml conversation: @@ -102,6 +104,10 @@ integrations_hook_create_payload: integrations_hook_update_payload: $ref: ./request/integrations/hook_update_payload.yml +# Automation Rule +automation_rule_create_update_payload: + $ref: ./request/automation_rule/create_update_payload.yml + ## public requests public_contact_create_update_payload: diff --git a/swagger/definitions/request/automation_rule/create_update_payload.yml b/swagger/definitions/request/automation_rule/create_update_payload.yml new file mode 100644 index 000000000..20d362e48 --- /dev/null +++ b/swagger/definitions/request/automation_rule/create_update_payload.yml @@ -0,0 +1,41 @@ +type: object +properties: + name: + type: string + description: Rule name + example: Add label on message create event + description: + type: string + description: The description about the automation and actions + example: Add label support and sales on message create event if incoming message content contains text help + event_name: + type: string + enum: + - conversation_created + - conversation_updated + - message_created + example: message_created + description: The event when you want to execute the automation actions + active: + type: boolean + description: Enable/disable automation rule + actions: + type: array + description: Array of actions which you want to perform when condition matches, e.g add label support if message contains content help. + items: + type: object + example: + action_name: add_label + action_params: + - support + conditions: + type: array + description: Array of conditions on which conversation filter would work, e.g message content contains text help. + items: + type: object + example: + attribute_key: content + filter_operator: contains + query_operator: nil + values: + - help diff --git a/swagger/definitions/resource/automation_rule.yml b/swagger/definitions/resource/automation_rule.yml new file mode 100644 index 000000000..4a3a03d01 --- /dev/null +++ b/swagger/definitions/resource/automation_rule.yml @@ -0,0 +1,45 @@ +type: object +properties: + event_name: + type: string + description: Automation Rule event, on which we call the actions(conversation_created, conversation_updated, message_created) + enum: + - conversation_created + - conversation_updated + - message_created + example: message_created + name: + type: string + description: The name of the rule + example: Add label on message create event + description: + type: string + description: Description to give more context about the rule + example: Add label support and sales on message create event if incoming message content contains text help + active: + type: boolean + description: Enable/disable automation rule + actions: + type: array + description: Array of actions which we perform when condition matches + items: + type: object + example: + action_name: add_label + action_params: + - support + - sales + conditions: + type: array + description: Array of conditions on which conversation/message filter would work + items: + type: object + example: + attribute_key: content + filter_operator: contains + values: + - help + query_operator: nil + account_id: + type: integer + description: Account Id diff --git a/swagger/definitions/resource/contact.yml b/swagger/definitions/resource/contact.yml index e2462c08b..2e72a9834 100644 --- a/swagger/definitions/resource/contact.yml +++ b/swagger/definitions/resource/contact.yml @@ -15,6 +15,10 @@ properties: additional_attributes: type: object description: The object containing additional attributes related to the contact + custom_attributes: + type: object + description: The object to save custom attributes for contact, accepts custom attributes key and value + example: { attribute_key: attribute_value, signed_up_at: dd/mm/yyyy } contact_inboxes: type: array items: diff --git a/swagger/definitions/resource/conversation.yml b/swagger/definitions/resource/conversation.yml index 24c27f681..085d21879 100644 --- a/swagger/definitions/resource/conversation.yml +++ b/swagger/definitions/resource/conversation.yml @@ -30,3 +30,7 @@ properties: additional_attributes: type: object description: The object containing additional attributes related to the conversation + custom_attributes: + type: object + description: The object to save custom attributes for conversation, accepts custom attributes key and value + example: { attribute_key: attribute_value, priority_conversation_number: 3 } diff --git a/swagger/index.yml b/swagger/index.yml index f64a090e7..3dc245ddf 100644 --- a/swagger/index.yml +++ b/swagger/index.yml @@ -68,6 +68,7 @@ x-tagGroups: - Reports - Teams - Webhooks + - Automation Rule - name: Client tags: - Contacts API diff --git a/swagger/paths/application/automation_rule/create.yml b/swagger/paths/application/automation_rule/create.yml new file mode 100644 index 000000000..ac5de803a --- /dev/null +++ b/swagger/paths/application/automation_rule/create.yml @@ -0,0 +1,20 @@ +tags: + - Automation Rule +operationId: add-new-automation-rule-to-account +summary: Add a new automation rule +description: Add a new automation rule to account +security: + - userApiKey: [] +parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/automation_rule_create_update_payload' +responses: + '200': + description: Success + schema: + $ref: '#/definitions/automation_rule' + '403': + description: Access denied diff --git a/swagger/paths/application/automation_rule/delete.yml b/swagger/paths/application/automation_rule/delete.yml new file mode 100644 index 000000000..f82647d1d --- /dev/null +++ b/swagger/paths/application/automation_rule/delete.yml @@ -0,0 +1,20 @@ +tags: + - Automation Rule +operationId: delete-automation-rule-from-account +summary: Remove a automation rule from account +description: Remove a automation rule from account +security: + - userApiKey: [] +parameters: + - in: path + name: id + type: integer + required: true + description: The ID of the automation rule to be deleted +responses: + '200': + description: Success + '403': + description: Access denied + '404': + description: automation rule not found diff --git a/swagger/paths/application/automation_rule/index.yml b/swagger/paths/application/automation_rule/index.yml new file mode 100644 index 000000000..031696aeb --- /dev/null +++ b/swagger/paths/application/automation_rule/index.yml @@ -0,0 +1,20 @@ +tags: + - Automation Rule +operationId: get-account-automation-rule +summary: List all automation rules in an account +parameters: + - $ref: '#/parameters/account_id' + - $ref: '#/parameters/page' +description: Get details of automation rules in an Account +security: + - userApiKey: [] +responses: + '200': + description: Success + schema: + type: array + description: Array of all automation rules + items: + $ref: '#/definitions/automation_rule' + '403': + description: Access denied diff --git a/swagger/paths/application/automation_rule/show.yml b/swagger/paths/application/automation_rule/show.yml new file mode 100644 index 000000000..d99f8170b --- /dev/null +++ b/swagger/paths/application/automation_rule/show.yml @@ -0,0 +1,20 @@ +tags: + - Automation Rule +operationId: get-details-of-a-single-automation-rule +summary: Get a automation rule details +description: Get the details of a automation rule in the account +parameters: + - in: path + name: id + type: integer + required: true + description: The ID of the automation rule to be updated. +responses: + '200': + description: Success + schema: + $ref: '#/definitions/automation_rule' + '401': + description: Unauthorized + '404': + description: The given rule ID does not exist in the account diff --git a/swagger/paths/application/automation_rule/update.yml b/swagger/paths/application/automation_rule/update.yml new file mode 100644 index 000000000..35a4cb67a --- /dev/null +++ b/swagger/paths/application/automation_rule/update.yml @@ -0,0 +1,27 @@ +tags: + - Automation Rule +operationId: update-automation-rule-in-account +summary: Update automation rule in Account +description: Update a automation rule in account +security: + - userApiKey: [] +parameters: + - in: path + name: id + type: integer + required: true + description: The ID of the automation rule to be updated. + - name: data + in: body + required: true + schema: + $ref: '#/definitions/automation_rule_create_update_payload' +responses: + '200': + description: Success + schema: + $ref: '#/definitions/automation_rule' + '403': + description: Access denied + '404': + description: Rule not found diff --git a/swagger/paths/application/conversation/index.yml b/swagger/paths/application/conversation/index.yml index 54650f4d2..4c9241464 100644 --- a/swagger/paths/application/conversation/index.yml +++ b/swagger/paths/application/conversation/index.yml @@ -40,7 +40,7 @@ get: type: integer default: 1 description: paginate through conversations - + responses: 200: description: Success @@ -79,6 +79,10 @@ post: additional_attributes: type: object description: Lets you specify attributes like browser information + custom_attributes: + type: object + description: The object to save custom attributes for conversation, accepts custom attributes key and value + example: { attribute_key: attribute_value, priority_conversation_number: 3 } status: type: string enum: ['open', 'resolved', 'pending'] diff --git a/swagger/paths/application/custom_attributes/delete.yml b/swagger/paths/application/custom_attributes/delete.yml index cfdb68adc..0c9aaedee 100644 --- a/swagger/paths/application/custom_attributes/delete.yml +++ b/swagger/paths/application/custom_attributes/delete.yml @@ -6,6 +6,7 @@ description: Remove a custom attribute from account security: - userApiKey: [] parameters: + - $ref: '#/parameters/account_id' - in: path name: id type: integer diff --git a/swagger/paths/application/custom_attributes/show.yml b/swagger/paths/application/custom_attributes/show.yml index 4e6f61e49..8449ba9be 100644 --- a/swagger/paths/application/custom_attributes/show.yml +++ b/swagger/paths/application/custom_attributes/show.yml @@ -3,6 +3,13 @@ tags: operationId: get-details-of-a-single-custom-attribute summary: Get a custom attribute details description: Get the details of a custom attribute in the account +parameters: + - $ref: '#/parameters/account_id' + - in: path + name: id + type: integer + required: true + description: The ID of the custom attribute to be updated. responses: 200: description: Success diff --git a/swagger/paths/application/team_members/index.yml b/swagger/paths/application/team_members/index.yml index dc28f2e99..c57705223 100644 --- a/swagger/paths/application/team_members/index.yml +++ b/swagger/paths/application/team_members/index.yml @@ -6,6 +6,7 @@ description: Get Details of Agents in an Team security: - userApiKey: [] parameters: + - $ref: '#/parameters/account_id' - $ref: '#/parameters/team_id' responses: 200: diff --git a/swagger/paths/index.yml b/swagger/paths/index.yml index 084065a9a..a7b853cb8 100644 --- a/swagger/paths/index.yml +++ b/swagger/paths/index.yml @@ -181,6 +181,11 @@ /api/v1/accounts/{account_id}/custom_attribute_definitions/{id}: parameters: - $ref: '#/parameters/account_id' + - name: id + in: path + type: number + description: ID of the custom attribute + required: true get: $ref: './application/custom_attributes/show.yml' patch: @@ -208,6 +213,30 @@ $ref: ./application/contactable_inboxes/get.yml +# Automation Rule +/api/v1/accounts/{account_id}/automation_rule_definitions: + parameters: + - $ref: '#/parameters/account_id' + get: + $ref: ./application/automation_rule/index.yml + post: + $ref: ./application/automation_rule/create.yml +/api/v1/accounts/{account_id}/automation_rule_definitions/{id}: + parameters: + - $ref: '#/parameters/account_id' + - name: id + in: path + type: number + description: ID of the Automation Rule + required: true + get: + $ref: ./application/automation_rule/show.yml + patch: + $ref: ./application/automation_rule/update.yml + delete: + $ref: ./application/automation_rule/delete.yml + + # Conversations /api/v1/accounts/{account_id}/conversations/meta: $ref: ./application/conversation/meta.yml @@ -273,8 +302,6 @@ - $ref: '#/parameters/inbox_id' get: $ref: ./application/inboxes/inbox_members/show.yml - patch: - $ref: ./application/inboxes/inbox_members/update.yml delete: $ref: ./application/inboxes/inbox_members/delete.yml @@ -283,6 +310,8 @@ - $ref: '#/parameters/account_id' post: $ref: ./application/inboxes/inbox_members/create.yml + patch: + $ref: ./application/inboxes/inbox_members/update.yml @@ -345,7 +374,7 @@ $ref: ./application/teams/update.yml delete: $ref: ./application/teams/delete.yml -/accounts/{account-id}/teams/{team_id}/team_members: +/accounts/{account_id}/teams/{team_id}/team_members: parameters: - $ref: '#/parameters/account_id' - $ref: '#/parameters/team_id' diff --git a/swagger/swagger.json b/swagger/swagger.json index 9103fe13e..483aa3402 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -1584,6 +1584,13 @@ "parameters": [ { "$ref": "#/parameters/account_id" + }, + { + "name": "id", + "in": "path", + "type": "number", + "description": "ID of the custom attribute", + "required": true } ], "get": { @@ -1593,6 +1600,18 @@ "operationId": "get-details-of-a-single-custom-attribute", "summary": "Get a custom attribute details", "description": "Get the details of a custom attribute in the account", + "parameters": [ + { + "$ref": "#/parameters/account_id" + }, + { + "in": "path", + "name": "id", + "type": "integer", + "required": true, + "description": "The ID of the custom attribute to be updated." + } + ], "responses": { "200": { "description": "Success", @@ -1669,6 +1688,9 @@ } ], "parameters": [ + { + "$ref": "#/parameters/account_id" + }, { "in": "path", "name": "id", @@ -2135,6 +2157,213 @@ } } }, + "/api/v1/accounts/{account_id}/automation_rule_definitions": { + "parameters": [ + { + "$ref": "#/parameters/account_id" + } + ], + "get": { + "tags": [ + "Automation Rule" + ], + "operationId": "get-account-automation-rule", + "summary": "List all automation rules in an account", + "parameters": [ + { + "$ref": "#/parameters/account_id" + }, + { + "$ref": "#/parameters/page" + } + ], + "description": "Get details of automation rules in an Account", + "security": [ + { + "userApiKey": [ + + ] + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "type": "array", + "description": "Array of all automation rules", + "items": { + "$ref": "#/definitions/automation_rule" + } + } + }, + "403": { + "description": "Access denied" + } + } + }, + "post": { + "tags": [ + "Automation Rule" + ], + "operationId": "add-new-automation-rule-to-account", + "summary": "Add a new automation rule", + "description": "Add a new automation rule to account", + "security": [ + { + "userApiKey": [ + + ] + } + ], + "parameters": [ + { + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/automation_rule_create_update_payload" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/automation_rule" + } + }, + "403": { + "description": "Access denied" + } + } + } + }, + "/api/v1/accounts/{account_id}/automation_rule_definitions/{id}": { + "parameters": [ + { + "$ref": "#/parameters/account_id" + }, + { + "name": "id", + "in": "path", + "type": "number", + "description": "ID of the Automation Rule", + "required": true + } + ], + "get": { + "tags": [ + "Automation Rule" + ], + "operationId": "get-details-of-a-single-automation-rule", + "summary": "Get a automation rule details", + "description": "Get the details of a automation rule in the account", + "parameters": [ + { + "in": "path", + "name": "id", + "type": "integer", + "required": true, + "description": "The ID of the automation rule to be updated." + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/automation_rule" + } + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "The given rule ID does not exist in the account" + } + } + }, + "patch": { + "tags": [ + "Automation Rule" + ], + "operationId": "update-automation-rule-in-account", + "summary": "Update automation rule in Account", + "description": "Update a automation rule in account", + "security": [ + { + "userApiKey": [ + + ] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "type": "integer", + "required": true, + "description": "The ID of the automation rule to be updated." + }, + { + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/automation_rule_create_update_payload" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/automation_rule" + } + }, + "403": { + "description": "Access denied" + }, + "404": { + "description": "Rule not found" + } + } + }, + "delete": { + "tags": [ + "Automation Rule" + ], + "operationId": "delete-automation-rule-from-account", + "summary": "Remove a automation rule from account", + "description": "Remove a automation rule from account", + "security": [ + { + "userApiKey": [ + + ] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "type": "integer", + "required": true, + "description": "The ID of the automation rule to be deleted" + } + ], + "responses": { + "200": { + "description": "Success" + }, + "403": { + "description": "Access denied" + }, + "404": { + "description": "automation rule not found" + } + } + } + }, "/api/v1/accounts/{account_id}/conversations/meta": { "parameters": [ { @@ -2352,6 +2581,14 @@ "type": "object", "description": "Lets you specify attributes like browser information" }, + "custom_attributes": { + "type": "object", + "description": "The object to save custom attributes for conversation, accepts custom attributes key and value", + "example": { + "attribute_key": "attribute_value", + "priority_conversation_number": 3 + } + }, "status": { "type": "string", "enum": [ @@ -3101,69 +3338,6 @@ } } }, - "patch": { - "tags": [ - "Inboxes" - ], - "operationId": "update-agents-in-inbox", - "summary": "Update Agents in Inbox", - "description": "All agents except the one passed in params will be removed", - "security": [ - { - "userApiKey": [ - - ] - } - ], - "parameters": [ - { - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "object", - "required": [ - "inbox_id", - "user_ids" - ], - "properties": { - "inbox_id": { - "type": "string", - "description": "The ID of the inbox" - }, - "user_ids": { - "type": "array", - "items": { - "type": "integer" - }, - "description": "IDs of users to be added to the inbox" - } - } - } - } - ], - "responses": { - "200": { - "description": "Success", - "schema": { - "type": "array", - "description": "Array of all active agents", - "items": { - "$ref": "#/definitions/agent" - } - } - }, - "404": { - "description": "Inbox not found" - }, - "403": { - "description": "Access denied" - }, - "422": { - "description": "User must exist" - } - } - }, "delete": { "tags": [ "Inboxes" @@ -3289,6 +3463,69 @@ "description": "User must exist" } } + }, + "patch": { + "tags": [ + "Inboxes" + ], + "operationId": "update-agents-in-inbox", + "summary": "Update Agents in Inbox", + "description": "All agents except the one passed in params will be removed", + "security": [ + { + "userApiKey": [ + + ] + } + ], + "parameters": [ + { + "name": "data", + "in": "body", + "required": true, + "schema": { + "type": "object", + "required": [ + "inbox_id", + "user_ids" + ], + "properties": { + "inbox_id": { + "type": "string", + "description": "The ID of the inbox" + }, + "user_ids": { + "type": "array", + "items": { + "type": "integer" + }, + "description": "IDs of users to be added to the inbox" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "type": "array", + "description": "Array of all active agents", + "items": { + "$ref": "#/definitions/agent" + } + } + }, + "404": { + "description": "Inbox not found" + }, + "403": { + "description": "Access denied" + }, + "422": { + "description": "User must exist" + } + } } }, "/api/v1/accounts/{account_id}/conversations/{conversation_id}/messages": { @@ -3711,7 +3948,7 @@ } } }, - "/accounts/{account-id}/teams/{team_id}/team_members": { + "/accounts/{account_id}/teams/{team_id}/team_members": { "parameters": [ { "$ref": "#/parameters/account_id" @@ -3735,6 +3972,9 @@ } ], "parameters": [ + { + "$ref": "#/parameters/account_id" + }, { "$ref": "#/parameters/team_id" } @@ -4520,6 +4760,68 @@ } } }, + "automation_rule": { + "type": "object", + "properties": { + "event_name": { + "type": "string", + "description": "Automation Rule event, on which we call the actions(conversation_created, conversation_updated, message_created)", + "enum": [ + "conversation_created", + "conversation_updated", + "message_created" + ], + "example": "message_created" + }, + "name": { + "type": "string", + "description": "The name of the rule", + "example": "Add label on message create event" + }, + "description": { + "type": "string", + "description": "Description to give more context about the rule", + "example": "Add label support and sales on message create event if incoming message content contains text help" + }, + "active": { + "type": "boolean", + "description": "Enable/disable automation rule" + }, + "actions": { + "type": "array", + "description": "Array of actions which we perform when condition matches", + "items": { + "type": "object", + "example": { + "action_name": "add_label", + "action_params": [ + "support", + "sales" + ] + } + } + }, + "conditions": { + "type": "array", + "description": "Array of conditions on which conversation/message filter would work", + "items": { + "type": "object", + "example": { + "attribute_key": "content", + "filter_operator": "contains", + "values": [ + "help" + ], + "query_operator": "nil" + } + } + }, + "account_id": { + "type": "integer", + "description": "Account Id" + } + } + }, "contact": { "type": "object", "properties": { @@ -4543,6 +4845,14 @@ "type": "object", "description": "The object containing additional attributes related to the contact" }, + "custom_attributes": { + "type": "object", + "description": "The object to save custom attributes for contact, accepts custom attributes key and value", + "example": { + "attribute_key": "attribute_value", + "signed_up_at": "dd/mm/yyyy" + } + }, "contact_inboxes": { "type": "array", "items": { @@ -4598,6 +4908,14 @@ "additional_attributes": { "type": "object", "description": "The object containing additional attributes related to the conversation" + }, + "custom_attributes": { + "type": "object", + "description": "The object to save custom attributes for conversation, accepts custom attributes key and value", + "example": { + "attribute_key": "attribute_value", + "priority_conversation_number": 3 + } } } }, @@ -5428,6 +5746,63 @@ } } }, + "automation_rule_create_update_payload": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Rule name", + "example": "Add label on message create event" + }, + "description": { + "type": "string", + "description": "The description about the automation and actions", + "example": "Add label support and sales on message create event if incoming message content contains text help" + }, + "event_name": { + "type": "string", + "enum": [ + "conversation_created", + "conversation_updated", + "message_created" + ], + "example": "message_created", + "description": "The event when you want to execute the automation actions" + }, + "active": { + "type": "boolean", + "description": "Enable/disable automation rule" + }, + "actions": { + "type": "array", + "description": "Array of actions which you want to perform when condition matches, e.g add label support if message contains content help.", + "items": { + "type": "object", + "example": { + "action_name": "add_label", + "action_params": [ + "support" + ] + } + } + }, + "conditions": { + "type": "array", + "description": "Array of conditions on which conversation filter would work, e.g message content contains text help.", + "items": { + "type": "object", + "example": { + "attribute_key": "content", + "filter_operator": "contains", + "query_operator": "nil", + "values": [ + "help" + ] + } + } + } + } + }, "public_contact_create_update_payload": { "type": "object", "properties": { @@ -6016,7 +6391,8 @@ "Profile", "Reports", "Teams", - "Webhooks" + "Webhooks", + "Automation Rule" ] }, { From f0db8545cbd80f522ef7ba7f25ed9d29a63344f0 Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Tue, 14 Jun 2022 10:55:16 +0530 Subject: [PATCH 165/177] fix: Update profile settings header title (#4856) --- .../routes/dashboard/settings/profile/profile.routes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/profile.routes.js b/app/javascript/dashboard/routes/dashboard/settings/profile/profile.routes.js index 032b13d6e..7a667f2ec 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/profile/profile.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/profile/profile.routes.js @@ -10,7 +10,7 @@ export default { roles: ['administrator', 'agent'], component: SettingsContent, props: { - headerTitle: 'GENERAL_SETTINGS.TITLE', + headerTitle: 'PROFILE_SETTINGS.TITLE', icon: 'edit', showNewButton: false, }, From 1bb0371c1d6b93ec3a33270c94b0e106cf27e206 Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Tue, 14 Jun 2022 18:05:37 +0530 Subject: [PATCH 166/177] feat: Add agent_reply_time_window in API channels (#4857) --- .../widgets/WootWriter/ReplyBottomPanel.vue | 5 +- .../widgets/conversation/MessagesView.vue | 58 +++++++++++++------ app/models/channel/api.rb | 14 +++++ app/models/channel/facebook_page.rb | 2 +- app/models/channel/twilio_sms.rb | 2 +- app/models/channel/whatsapp.rb | 2 +- app/models/concerns/channelable.rb | 2 +- app/models/conversation.rb | 15 ++--- spec/models/channel/twilio_sms_spec.rb | 4 +- spec/models/conversation_spec.rb | 48 +++++++++++++++ 10 files changed, 116 insertions(+), 36 deletions(-) diff --git a/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue b/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue index e7a0b5995..636601dcb 100644 --- a/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue +++ b/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue @@ -80,7 +80,7 @@ @click="toggleMessageSignature" />
- - provider_config['api_key'], 'Content-Type' => 'application/json' } end - def has_24_hour_messaging_window? + def messaging_window_enabled? true end diff --git a/app/models/concerns/channelable.rb b/app/models/concerns/channelable.rb index e60066d5f..e54d5f3a6 100644 --- a/app/models/concerns/channelable.rb +++ b/app/models/concerns/channelable.rb @@ -6,7 +6,7 @@ module Channelable has_one :inbox, as: :channel, dependent: :destroy_async end - def has_24_hour_messaging_window? + def messaging_window_enabled? false end end diff --git a/app/models/conversation.rb b/app/models/conversation.rb index cd45adf7b..4791ba924 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -93,23 +93,24 @@ class Conversation < ApplicationRecord delegate :auto_resolve_duration, to: :account def can_reply? + channel = inbox&.channel + return can_reply_on_instagram? if additional_attributes['type'] == 'instagram_direct_message' - return true unless inbox&.channel&.has_24_hour_messaging_window? + return true unless channel&.messaging_window_enabled? - return false if last_incoming_message.nil? - - last_message_less_than_24_hrs? + messaging_window = inbox.api? ? channel.additional_attributes['agent_reply_time_window'].to_i : 24 + last_message_in_messaging_window?(messaging_window) end def last_incoming_message messages&.incoming&.last end - def last_message_less_than_24_hrs? + def last_message_in_messaging_window?(time) return false if last_incoming_message.nil? - Time.current < last_incoming_message.created_at + 24.hours + Time.current < last_incoming_message.created_at + time.hours end def can_reply_on_instagram? @@ -120,7 +121,7 @@ class Conversation < ApplicationRecord if global_config['ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT'] Time.current < last_incoming_message.created_at + 7.days else - last_message_less_than_24_hrs? + last_message_in_messaging_window?(24) end end diff --git a/spec/models/channel/twilio_sms_spec.rb b/spec/models/channel/twilio_sms_spec.rb index 8aab34a7a..493c644ac 100644 --- a/spec/models/channel/twilio_sms_spec.rb +++ b/spec/models/channel/twilio_sms_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Channel::TwilioSms do let!(:whatsapp_channel) { create(:channel_twilio_sms, medium: :whatsapp) } it 'returns true' do - expect(whatsapp_channel.has_24_hour_messaging_window?).to eq true + expect(whatsapp_channel.messaging_window_enabled?).to eq true expect(whatsapp_channel.name).to eq 'Whatsapp' expect(whatsapp_channel.medium).to eq 'whatsapp' end @@ -17,7 +17,7 @@ RSpec.describe Channel::TwilioSms do let!(:sms_channel) { create(:channel_twilio_sms, medium: :sms) } it 'returns false' do - expect(sms_channel.has_24_hour_messaging_window?).to eq false + expect(sms_channel.messaging_window_enabled?).to eq false expect(sms_channel.name).to eq 'Twilio SMS' expect(sms_channel.medium).to eq 'sms' end diff --git a/spec/models/conversation_spec.rb b/spec/models/conversation_spec.rb index e83ee5195..1b1e84ea5 100644 --- a/spec/models/conversation_spec.rb +++ b/spec/models/conversation_spec.rb @@ -545,6 +545,54 @@ RSpec.describe Conversation, type: :model do end end end + + describe 'on API channels' do + let!(:api_channel) { create(:channel_api, additional_attributes: {}) } + let!(:api_channel_with_limit) { create(:channel_api, additional_attributes: { agent_reply_time_window: '12' }) } + + context 'when agent_reply_time_window is not configured' do + it 'return true irrespective of the last message time' do + conversation = create(:conversation, inbox: api_channel.inbox) + create( + :message, + account: conversation.account, + inbox: api_channel.inbox, + conversation: conversation, + created_at: Time.now - 13.hours + ) + + expect(api_channel.additional_attributes['agent_reply_time_window']).to eq nil + expect(conversation.can_reply?).to eq true + end + end + + context 'when agent_reply_time_window is configured' do + it 'return false if it is outside of agent_reply_time_window' do + conversation = create(:conversation, inbox: api_channel_with_limit.inbox) + create( + :message, + account: conversation.account, + inbox: api_channel_with_limit.inbox, + conversation: conversation, + created_at: Time.now - 13.hours + ) + + expect(api_channel_with_limit.additional_attributes['agent_reply_time_window']).to eq '12' + expect(conversation.can_reply?).to eq false + end + + it 'return true if it is inside of agent_reply_time_window' do + conversation = create(:conversation, inbox: api_channel_with_limit.inbox) + create( + :message, + account: conversation.account, + inbox: api_channel_with_limit.inbox, + conversation: conversation + ) + expect(conversation.can_reply?).to eq true + end + end + end end describe '#delete conversation' do From fdcaed75f654eb247769160acbb05012298bc6bd Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Tue, 14 Jun 2022 23:46:36 +0530 Subject: [PATCH 167/177] chore: Move Whatsapp template sync to cron (#4858) syncing WhatsApp templates job is moved to a cron job for a better user experience. The Templates are synced at 15-minute intervals now. --- .../channels/whatsapp/templates_sync_job.rb | 7 +++++ .../whatsapp/templates_sync_scheduler_job.rb | 12 +++++++++ app/jobs/trigger_scheduled_items_job.rb | 3 +++ app/models/channel/whatsapp.rb | 5 +--- .../whatsapp/send_on_whatsapp_service.rb | 1 - spec/factories/channel/channel_whatsapp.rb | 9 +++++++ .../whatsapp/templates_sync_job_spec.rb | 20 ++++++++++++++ .../templates_sync_scheduler_job_spec.rb | 27 +++++++++++++++++++ spec/jobs/send_reply_job_spec.rb | 4 +-- spec/jobs/trigger_scheduled_items_job_spec.rb | 5 ++++ .../whatsapp/incoming_message_service_spec.rb | 2 +- .../whatsapp/send_on_whatsapp_service_spec.rb | 2 +- 12 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 app/jobs/channels/whatsapp/templates_sync_job.rb create mode 100644 app/jobs/channels/whatsapp/templates_sync_scheduler_job.rb create mode 100644 spec/jobs/channels/whatsapp/templates_sync_job_spec.rb create mode 100644 spec/jobs/channels/whatsapp/templates_sync_scheduler_job_spec.rb diff --git a/app/jobs/channels/whatsapp/templates_sync_job.rb b/app/jobs/channels/whatsapp/templates_sync_job.rb new file mode 100644 index 000000000..a9ea4616c --- /dev/null +++ b/app/jobs/channels/whatsapp/templates_sync_job.rb @@ -0,0 +1,7 @@ +class Channels::Whatsapp::TemplatesSyncJob < ApplicationJob + queue_as :low + + def perform(whatsapp_channel) + whatsapp_channel.sync_templates + end +end diff --git a/app/jobs/channels/whatsapp/templates_sync_scheduler_job.rb b/app/jobs/channels/whatsapp/templates_sync_scheduler_job.rb new file mode 100644 index 000000000..1b11c32c4 --- /dev/null +++ b/app/jobs/channels/whatsapp/templates_sync_scheduler_job.rb @@ -0,0 +1,12 @@ +class Channels::Whatsapp::TemplatesSyncSchedulerJob < ApplicationJob + queue_as :low + + def perform + Channel::Whatsapp.where('message_templates_last_updated <= ? OR message_templates_last_updated IS NULL', + 15.minutes.ago).find_in_batches do |channels_batch| + channels_batch.each do |channel| + Channels::Whatsapp::TemplatesSyncJob.perform_later(channel) + end + end + end +end diff --git a/app/jobs/trigger_scheduled_items_job.rb b/app/jobs/trigger_scheduled_items_job.rb index 5d1b89727..a3a502ec0 100644 --- a/app/jobs/trigger_scheduled_items_job.rb +++ b/app/jobs/trigger_scheduled_items_job.rb @@ -12,5 +12,8 @@ class TriggerScheduledItemsJob < ApplicationJob # Job to auto-resolve conversations Account::ConversationsResolutionSchedulerJob.perform_later + + # Job to sync whatsapp templates + Channels::Whatsapp::TemplatesSyncSchedulerJob.perform_later end end diff --git a/app/models/channel/whatsapp.rb b/app/models/channel/whatsapp.rb index a97dcc930..1f7a14c39 100644 --- a/app/models/channel/whatsapp.rb +++ b/app/models/channel/whatsapp.rb @@ -25,6 +25,7 @@ class Channel::Whatsapp < ApplicationRecord validates :phone_number, presence: true, uniqueness: true before_save :validate_provider_config + after_create :sync_templates def name 'Whatsapp' @@ -62,10 +63,6 @@ class Channel::Whatsapp < ApplicationRecord end def sync_templates - # to prevent too many api calls - last_updated = message_templates_last_updated || 1.day.ago - return if Time.current < (last_updated + 12.hours) - response = HTTParty.get("#{api_base_path}/configs/templates", headers: api_headers) update(message_templates: response['waba_templates'], message_templates_last_updated: Time.now.utc) if response.success? end diff --git a/app/services/whatsapp/send_on_whatsapp_service.rb b/app/services/whatsapp/send_on_whatsapp_service.rb index be81c332b..efba78f7e 100644 --- a/app/services/whatsapp/send_on_whatsapp_service.rb +++ b/app/services/whatsapp/send_on_whatsapp_service.rb @@ -15,7 +15,6 @@ class Whatsapp::SendOnWhatsappService < Base::SendOnChannelService end def send_template_message - channel.sync_templates name, namespace, lang_code, processed_parameters = processable_channel_message_template return if name.blank? diff --git a/spec/factories/channel/channel_whatsapp.rb b/spec/factories/channel/channel_whatsapp.rb index 0c60cc78f..3db99b47a 100644 --- a/spec/factories/channel/channel_whatsapp.rb +++ b/spec/factories/channel/channel_whatsapp.rb @@ -34,6 +34,15 @@ FactoryBot.define do end message_templates_last_updated { Time.now.utc } + transient do + sync_templates { true } + end + + before(:create) do |channel_whatsapp, options| + # since factory already has the required message templates, we just need to bypass it getting updated + channel_whatsapp.define_singleton_method(:sync_templates) { return } unless options.sync_templates + end + after(:create) do |channel_whatsapp| create(:inbox, channel: channel_whatsapp, account: channel_whatsapp.account) end diff --git a/spec/jobs/channels/whatsapp/templates_sync_job_spec.rb b/spec/jobs/channels/whatsapp/templates_sync_job_spec.rb new file mode 100644 index 000000000..335e4d6f7 --- /dev/null +++ b/spec/jobs/channels/whatsapp/templates_sync_job_spec.rb @@ -0,0 +1,20 @@ +require 'rails_helper' + +RSpec.describe Channels::Whatsapp::TemplatesSyncJob, type: :job do + let(:channel_whatsapp) { create(:channel_whatsapp, sync_templates: false) } + + it 'enqueues the job' do + stub_request(:post, 'https://waba.360dialog.io/v1/configs/webhook') + expect { described_class.perform_later(channel_whatsapp) }.to have_enqueued_job(described_class) + .on_queue('low') + end + + context 'when called' do + it 'calls sync_templates' do + whatsapp_channel = double + allow(whatsapp_channel).to receive(:sync_templates).and_return(true) + expect(whatsapp_channel).to receive(:sync_templates) + described_class.perform_now(whatsapp_channel) + end + end +end diff --git a/spec/jobs/channels/whatsapp/templates_sync_scheduler_job_spec.rb b/spec/jobs/channels/whatsapp/templates_sync_scheduler_job_spec.rb new file mode 100644 index 000000000..6d345584d --- /dev/null +++ b/spec/jobs/channels/whatsapp/templates_sync_scheduler_job_spec.rb @@ -0,0 +1,27 @@ +require 'rails_helper' + +RSpec.describe Channels::Whatsapp::TemplatesSyncSchedulerJob, type: :job do + it 'enqueues the job' do + expect { described_class.perform_later }.to have_enqueued_job(described_class) + .on_queue('low') + end + + context 'when called' do + it 'schedules templates_sync_jobs for channels where templates need to be updated' do + stub_request(:post, 'https://waba.360dialog.io/v1/configs/webhook') + non_synced = create(:channel_whatsapp, sync_templates: false, message_templates_last_updated: nil) + synced_recently = create(:channel_whatsapp, sync_templates: false, message_templates_last_updated: Time.zone.now) + synced_old = create(:channel_whatsapp, sync_templates: false, message_templates_last_updated: 16.minutes.ago) + described_class.perform_now + expect(Channels::Whatsapp::TemplatesSyncJob).not_to( + have_been_enqueued.with(synced_recently).on_queue('low') + ) + expect(Channels::Whatsapp::TemplatesSyncJob).to( + have_been_enqueued.with(synced_old).on_queue('low') + ) + expect(Channels::Whatsapp::TemplatesSyncJob).to( + have_been_enqueued.with(non_synced).on_queue('low') + ) + end + end +end diff --git a/spec/jobs/send_reply_job_spec.rb b/spec/jobs/send_reply_job_spec.rb index 03b249368..304653b41 100644 --- a/spec/jobs/send_reply_job_spec.rb +++ b/spec/jobs/send_reply_job_spec.rb @@ -66,9 +66,9 @@ RSpec.describe SendReplyJob, type: :job do described_class.perform_now(message.id) end - it 'calls ::Whatsapp:SendOnWhatsappService when its line message' do + it 'calls ::Whatsapp:SendOnWhatsappService when its whatsapp message' do stub_request(:post, 'https://waba.360dialog.io/v1/configs/webhook') - whatsapp_channel = create(:channel_whatsapp) + whatsapp_channel = create(:channel_whatsapp, sync_templates: false) message = create(:message, conversation: create(:conversation, inbox: whatsapp_channel.inbox)) allow(::Whatsapp::SendOnWhatsappService).to receive(:new).with(message: message).and_return(process_service) expect(::Whatsapp::SendOnWhatsappService).to receive(:new).with(message: message) diff --git a/spec/jobs/trigger_scheduled_items_job_spec.rb b/spec/jobs/trigger_scheduled_items_job_spec.rb index f94371e8b..e7b5fe597 100644 --- a/spec/jobs/trigger_scheduled_items_job_spec.rb +++ b/spec/jobs/trigger_scheduled_items_job_spec.rb @@ -30,5 +30,10 @@ RSpec.describe TriggerScheduledItemsJob, type: :job do expect(Account::ConversationsResolutionSchedulerJob).to receive(:perform_later).once described_class.perform_now end + + it 'triggers Channels::Whatsapp::TemplatesSyncSchedulerJob' do + expect(Channels::Whatsapp::TemplatesSyncSchedulerJob).to receive(:perform_later).once + described_class.perform_now + end end end diff --git a/spec/services/whatsapp/incoming_message_service_spec.rb b/spec/services/whatsapp/incoming_message_service_spec.rb index 0144dd169..efe9949f5 100644 --- a/spec/services/whatsapp/incoming_message_service_spec.rb +++ b/spec/services/whatsapp/incoming_message_service_spec.rb @@ -6,7 +6,7 @@ describe Whatsapp::IncomingMessageService do stub_request(:post, 'https://waba.360dialog.io/v1/configs/webhook') end - let!(:whatsapp_channel) { create(:channel_whatsapp) } + let!(:whatsapp_channel) { create(:channel_whatsapp, sync_templates: false) } context 'when valid text message params' do it 'creates appropriate conversations, message and contacts' do diff --git a/spec/services/whatsapp/send_on_whatsapp_service_spec.rb b/spec/services/whatsapp/send_on_whatsapp_service_spec.rb index b08f25931..1dc405abd 100644 --- a/spec/services/whatsapp/send_on_whatsapp_service_spec.rb +++ b/spec/services/whatsapp/send_on_whatsapp_service_spec.rb @@ -15,7 +15,7 @@ describe Whatsapp::SendOnWhatsappService do context 'when a valid message' do let(:whatsapp_request) { double } - let!(:whatsapp_channel) { create(:channel_whatsapp) } + let!(:whatsapp_channel) { create(:channel_whatsapp, sync_templates: false) } let!(:contact_inbox) { create(:contact_inbox, inbox: whatsapp_channel.inbox, source_id: '123456789') } let!(:conversation) { create(:conversation, contact_inbox: contact_inbox, inbox: whatsapp_channel.inbox) } From 067c9053297defe54328af8967e8ee2b95e98cb5 Mon Sep 17 00:00:00 2001 From: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com> Date: Wed, 15 Jun 2022 14:18:05 +0530 Subject: [PATCH 168/177] feat: Allow agents to bulk assign labels to conversations (#4854) Co-authored-by: Pranav Raj S --- .../dashboard/components/ChatList.vue | 16 + .../conversationBulkActions/AgentSelector.vue | 2 +- .../conversationBulkActions/Index.vue | 31 +- .../conversationBulkActions/LabelActions.vue | 282 ++++++++++++++++++ .../dashboard/i18n/locale/en/bulkActions.json | 7 + 5 files changed, 334 insertions(+), 4 deletions(-) create mode 100644 app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/LabelActions.vue diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue index 6f6b1961c..d1ac4945f 100644 --- a/app/javascript/dashboard/components/ChatList.vue +++ b/app/javascript/dashboard/components/ChatList.vue @@ -95,6 +95,7 @@ @select-all-conversations="selectAllConversations" @assign-agent="onAssignAgent" @update-conversations="onUpdateConversations" + @assign-labels="onAssignLabels" />
+
+ + + import AgentSelector from './AgentSelector.vue'; import UpdateActions from './UpdateActions.vue'; +import LabelActions from './LabelActions.vue'; export default { components: { AgentSelector, UpdateActions, + LabelActions, }, props: { conversations: { @@ -103,6 +121,7 @@ export default { return { showAgentsList: false, showUpdateActions: false, + showLabelActions: false, }; }, methods: { @@ -115,12 +134,18 @@ export default { updateConversations(status) { this.$emit('update-conversations', status); }, + assignLabels(labels) { + this.$emit('assign-labels', labels); + }, resolveConversations() { this.$emit('resolve-conversations'); }, toggleUpdateActions() { this.showUpdateActions = !this.showUpdateActions; }, + toggleLabelActions() { + this.showLabelActions = !this.showLabelActions; + }, toggleAgentList() { this.showAgentsList = !this.showAgentsList; }, diff --git a/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/LabelActions.vue b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/LabelActions.vue new file mode 100644 index 000000000..eb93586e3 --- /dev/null +++ b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/LabelActions.vue @@ -0,0 +1,282 @@ + + + + + diff --git a/app/javascript/dashboard/i18n/locale/en/bulkActions.json b/app/javascript/dashboard/i18n/locale/en/bulkActions.json index f9e084cd7..7061e5e70 100644 --- a/app/javascript/dashboard/i18n/locale/en/bulkActions.json +++ b/app/javascript/dashboard/i18n/locale/en/bulkActions.json @@ -17,6 +17,13 @@ "SNOOZE_UNTIL_NEXT_REPLY": "Snooze until next reply", "UPDATE_SUCCESFUL": "Conversation status updated successfully.", "UPDATE_FAILED": "Failed to update conversations, please try again" + }, + "LABELS": { + "ASSIGN_LABELS": "Assign Labels", + "NO_LABELS_FOUND": "No labels found for", + "ASSIGN_SELECTED_LABELS": "Assign selected labels", + "ASSIGN_SUCCESFUL": "Labels assigned successfully", + "ASSIGN_FAILED": "Failed to assign labels, please try again" } } } From 9015d83679aef81395c5ec829008a8e70febc02f Mon Sep 17 00:00:00 2001 From: "Aswin Dev P.S" Date: Wed, 15 Jun 2022 16:20:19 +0530 Subject: [PATCH 169/177] chore: Fix sentry issues (#4863) Fix sentry issues. Fixes #4815, #4814, #4811, #4809 --- .../api/v1/accounts/inboxes_controller.rb | 18 ++++++------ app/helpers/api/v1/inboxes_helper.rb | 28 +++++++++++++++++-- .../dashboard/settings/inbox/ImapSettings.vue | 2 +- .../dashboard/store/modules/inboxes.js | 6 +++- app/mailboxes/mailbox_helper.rb | 2 ++ config/locales/en.yml | 7 +++++ 6 files changed, 51 insertions(+), 12 deletions(-) diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index ded4db598..95662e29b 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -43,14 +43,18 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController def update @inbox.update!(permitted_params.except(:channel)) - @inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours] + update_inbox_working_hours channel_attributes = get_channel_attributes(@inbox.channel_type) # Inbox update doesn't necessarily need channel attributes return if permitted_params(channel_attributes)[:channel].blank? if @inbox.inbox_type == 'Email' - validate_email_channel(channel_attributes) + begin + validate_email_channel(channel_attributes) + rescue StandardError => e + render json: { message: e }, status: :unprocessable_entity and return + end @inbox.channel.reauthorized! end @@ -58,6 +62,10 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController update_channel_feature_flags end + def update_inbox_working_hours + @inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours] + end + def agent_bot @agent_bot = @inbox.agent_bot end @@ -89,12 +97,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController @agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot] end - def inbox_name(channel) - return channel.try(:bot_name) if channel.is_a?(Channel::Telegram) - - permitted_params[:name] - end - def create_channel return unless %w[web_widget api email line telegram whatsapp sms].include?(permitted_params[:channel][:type]) diff --git a/app/helpers/api/v1/inboxes_helper.rb b/app/helpers/api/v1/inboxes_helper.rb index 82644e62f..aa28e4ccc 100644 --- a/app/helpers/api/v1/inboxes_helper.rb +++ b/app/helpers/api/v1/inboxes_helper.rb @@ -1,4 +1,10 @@ module Api::V1::InboxesHelper + def inbox_name(channel) + return channel.try(:bot_name) if channel.is_a?(Channel::Telegram) + + permitted_params[:name] + end + def validate_email_channel(attributes) channel_data = permitted_params(attributes)[:channel] @@ -19,8 +25,7 @@ module Api::V1::InboxesHelper enable_ssl: channel_data[:imap_enable_ssl] } end - Mail.connection do # rubocop:disable:block - end + check_imap_connection(channel_data) end def validate_smtp(channel_data) @@ -32,6 +37,25 @@ module Api::V1::InboxesHelper check_smtp_connection(channel_data, smtp) end + def check_imap_connection(channel_data) + Mail.connection {} # rubocop:disable:block + rescue SocketError => e + raise StandardError, I18n.t('errors.inboxes.imap.socket_error') + rescue Net::IMAP::NoResponseError => e + raise StandardError, I18n.t('errors.inboxes.imap.no_response_error') + rescue Errno::EHOSTUNREACH => e + raise StandardError, I18n.t('errors.inboxes.imap.host_unreachable_error') + rescue Net::OpenTimeout => e + raise StandardError, + I18n.t('errors.inboxes.imap.connection_timed_out_error', address: channel_data[:imap_address], port: channel_data[:imap_port]) + rescue Net::IMAP::Error => e + raise StandardError, I18n.t('errors.inboxes.imap.connection_closed_error') + rescue StandardError => e + raise StandardError, e.message + ensure + Rails.logger.error e if e.present? + end + def check_smtp_connection(channel_data, smtp) smtp.start(channel_data[:smtp_domain], channel_data[:smtp_login], channel_data[:smtp_password], channel_data[:smtp_authentication]&.to_sym || :login) diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/ImapSettings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/ImapSettings.vue index af2d5dd5c..072fc20f3 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/ImapSettings.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/ImapSettings.vue @@ -156,7 +156,7 @@ export default { await this.$store.dispatch('inboxes/updateInboxIMAP', payload); this.showAlert(this.$t('INBOX_MGMT.IMAP.EDIT.SUCCESS_MESSAGE')); } catch (error) { - this.showAlert(this.$t('INBOX_MGMT.IMAP.EDIT.ERROR_MESSAGE')); + this.showAlert(error.message); } }, }, diff --git a/app/javascript/dashboard/store/modules/inboxes.js b/app/javascript/dashboard/store/modules/inboxes.js index b16ed4799..668107ef7 100644 --- a/app/javascript/dashboard/store/modules/inboxes.js +++ b/app/javascript/dashboard/store/modules/inboxes.js @@ -216,7 +216,11 @@ export const actions = { commit(types.default.SET_INBOXES_UI_FLAG, { isUpdatingIMAP: false, }); - throw new Error(error); + if (error.response?.data?.message) { + throw new Error(error.response?.data?.message); + } else { + throw new Error(error); + } } }, updateInboxSMTP: async ( diff --git a/app/mailboxes/mailbox_helper.rb b/app/mailboxes/mailbox_helper.rb index 927ab5eb5..6aaf4fde3 100644 --- a/app/mailboxes/mailbox_helper.rb +++ b/app/mailboxes/mailbox_helper.rb @@ -21,6 +21,8 @@ module MailboxHelper end def add_attachments_to_message + return if @message.blank? + processed_mail.attachments.each do |mail_attachment| attachment = @message.attachments.new( account_id: @conversation.account_id, diff --git a/config/locales/en.yml b/config/locales/en.yml index b986ef1c1..d7bf7c4e7 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -46,6 +46,13 @@ en: contacts: import: failed: File is blank + inboxes: + imap: + socket_error: Please check the network connection, IMAP address and try again. + no_response_error: Please check the IMAP credentials and try again. + host_unreachable_error: Host unreachable, Please check the IMAP address, IMAP port and try again. + connection_timed_out_error: Connection timed out for %{address}:%{port} + connection_closed_error: Connection closed. reports: period: Reporting period %{since} to %{until} From c2114e468e0110aef8d662ca87af6283fbfb2fbc Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Wed, 15 Jun 2022 12:06:15 +0000 Subject: [PATCH 170/177] Bump version to 2.6.0 --- config/app.yml | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/app.yml b/config/app.yml index 1ea45235d..8db207537 100644 --- a/config/app.yml +++ b/config/app.yml @@ -1,5 +1,5 @@ shared: &shared - version: '2.5.0' + version: '2.6.0' development: <<: *shared diff --git a/package.json b/package.json index a33a9045d..703f364fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@chatwoot/chatwoot", - "version": "2.5.0", + "version": "2.6.0", "license": "MIT", "scripts": { "eslint": "eslint app/**/*.{js,vue}", From 644f07a23d97726d9c80cfab4fd656cd9f53d282 Mon Sep 17 00:00:00 2001 From: Prithvi Tharun Date: Wed, 15 Jun 2022 18:34:24 +0530 Subject: [PATCH 171/177] chore: Copy change for automatic greeting message (#4727) fixes: #4726 --- app/javascript/dashboard/i18n/locale/en/inboxMgmt.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index 6bc5603b0..54e69c304 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -82,7 +82,7 @@ }, "CHANNEL_GREETING_TOGGLE": { "LABEL": "Enable channel greeting", - "HELP_TEXT": "Send a greeting message to the users when they starts the conversation.", + "HELP_TEXT": "Automatically send a greeting message when a new conversation is created.", "ENABLED": "Enabled", "DISABLED": "Disabled" }, From 205d42e978f38362decd75e4d661f090bfdadcbd Mon Sep 17 00:00:00 2001 From: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com> Date: Thu, 16 Jun 2022 11:02:32 +0530 Subject: [PATCH 172/177] fix: Bulk actions agent loading (#4874) --- .../conversationBulkActions/AgentSelector.vue | 10 +++++++--- .../conversationBulkActions/LabelActions.vue | 10 +++++----- app/javascript/shared/assets/stylesheets/spacing.scss | 3 ++- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/AgentSelector.vue b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/AgentSelector.vue index acf23d716..f7777e357 100644 --- a/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/AgentSelector.vue +++ b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/AgentSelector.vue @@ -22,7 +22,10 @@ />
-
+

{{ $t('BULK_ACTION.AGENT_LIST_LOADING') }}

@@ -171,7 +174,7 @@ export default { transform-origin: top right; width: auto; z-index: var(--z-index-twenty); - + min-width: var(--space-giga); .header { padding: var(--space-one); @@ -182,7 +185,7 @@ export default { } .container { - max-height: 24rem; + max-height: var(--space-giga); overflow-y: auto; .agent__list-container { height: 100%; @@ -264,5 +267,6 @@ ul { align-items: center; justify-content: center; flex-direction: column; + padding: var(--space-two); } diff --git a/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/LabelActions.vue b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/LabelActions.vue index eb93586e3..70f74199b 100644 --- a/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/LabelActions.vue +++ b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/LabelActions.vue @@ -110,7 +110,7 @@ export default { .labels-list { display: flex; flex-direction: column; - max-height: 24rem; + max-height: var(--space-giga); min-height: auto; .labels-list__header { @@ -157,8 +157,8 @@ export default { border-radius: var(--border-radius-large); border: 1px solid var(--s-50); box-shadow: var(--shadow-dropdown-pane); - max-width: 24rem; - min-width: 24rem; + max-width: var(--space-giga); + min-width: var(--space-giga); position: absolute; right: 4.5rem; top: var(--space-larger); @@ -176,7 +176,7 @@ export default { } .container { - max-height: 24rem; + max-height: var(--space-giga); overflow-y: auto; .label__list-container { @@ -204,7 +204,7 @@ export default { .triangle { display: block; position: absolute; - right: 2rem; + right: var(--space-two); text-align: left; top: calc(var(--space-slab) * -1); z-index: var(--z-index-one); diff --git a/app/javascript/shared/assets/stylesheets/spacing.scss b/app/javascript/shared/assets/stylesheets/spacing.scss index 9ce5fecb0..c556c65b8 100644 --- a/app/javascript/shared/assets/stylesheets/spacing.scss +++ b/app/javascript/shared/assets/stylesheets/spacing.scss @@ -15,7 +15,8 @@ --space-larger: 4.8rem; --space-jumbo: 6.4rem; --space-mega: 10rem; - + --space-giga: 24rem; + --space-minus-micro: -0.2rem; --space-minus-smaller: -0.4rem; --space-minus-small: -0.8rem; From db8626335368b915667ece05201e2205552c90be Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Fri, 17 Jun 2022 03:07:07 +0530 Subject: [PATCH 173/177] chore: Codespace Improvements (#4867) - switch to ubuntu base image - use rbenv to manage ruby - add gh to base image --- .devcontainer/Dockerfile | 2 +- .devcontainer/Dockerfile.base | 47 ++++++++++++++++++++++++++------- .devcontainer/devcontainer.json | 9 ++++--- .devcontainer/scripts/setup.sh | 5 ++++ 4 files changed, 48 insertions(+), 15 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index e3eb2fd01..2418bd621 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -5,4 +5,4 @@ FROM ghcr.io/chatwoot/chatwoot_codespace:latest # Do the set up required for chatwoot app WORKDIR /workspace COPY . /workspace -RUN yarn && gem install bundler && bundle install +RUN yarn && gem install bundler && bundle install diff --git a/.devcontainer/Dockerfile.base b/.devcontainer/Dockerfile.base index de007e202..769be24f8 100644 --- a/.devcontainer/Dockerfile.base +++ b/.devcontainer/Dockerfile.base @@ -1,6 +1,6 @@ -# pre-build stage -ARG VARIANT=3 -FROM mcr.microsoft.com/vscode/devcontainers/ruby:${VARIANT} + +ARG VARIANT=ubuntu-20.04 +FROM mcr.microsoft.com/vscode/devcontainers/base:${VARIANT} # Update args in docker-compose.yaml to set the UID/GID of the "vscode" user. ARG USER_UID=1000 @@ -11,23 +11,36 @@ RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \ && chmod -R $USER_UID:$USER_GID /home/vscode; \ fi -# [Option] Install Node.js -ARG INSTALL_NODE="true" -ARG NODE_VERSION="lts/*" -RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi - RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ && apt-get -y install --no-install-recommends \ + build-essential \ libssl-dev \ + zlib1g-dev \ + gnupg2 \ tar \ tzdata \ postgresql-client \ + libpq-dev \ yarn \ git \ imagemagick \ tmux \ - zsh + zsh \ + git-flow \ + npm +# Install rbenv and ruby +ARG RUBY_VERSION="3.0.4" +RUN git clone https://github.com/rbenv/rbenv.git ~/.rbenv \ + && echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc \ + && echo 'eval "$(rbenv init -)"' >> ~/.bashrc +ENV PATH "/root/.rbenv/bin/:/root/.rbenv/shims/:$PATH" +RUN git clone https://github.com/rbenv/ruby-build.git && \ + PREFIX=/usr/local ./ruby-build/install.sh + +RUN rbenv install $RUBY_VERSION && \ + rbenv global $RUBY_VERSION && \ + rbenv versions # Install overmind RUN curl -L https://github.com/DarthSim/overmind/releases/download/v2.1.0/overmind-v2.1.0-linux-amd64.gz > overmind.gz \ @@ -35,11 +48,25 @@ RUN curl -L https://github.com/DarthSim/overmind/releases/download/v2.1.0/overmi && sudo mv overmind /usr/local/bin \ && chmod +x /usr/local/bin/overmind + +# Install gh +RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ + && sudo apt update \ + && sudo apt install gh + + # Do the set up required for chatwoot app WORKDIR /workspace COPY . /workspace -RUN yarn +# set up ruby COPY Gemfile Gemfile.lock ./ RUN gem install bundler && bundle install +# set up node js +RUN npm install npm@latest -g && \ + npm install n -g && \ + n latest +RUN npm install --global yarn +RUN yarn diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c0301d87b..a70ba3788 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -23,17 +23,18 @@ // 5432 postgres // 6379 redis // 1025,8025 mailhog - "forwardPorts": [8025], - //your application may need to listen on all interfaces (0.0.0.0) not just localhost for it to be available externally. Defaults to [] - "appPort": [3000, 3035], + "forwardPorts": [8025, 3000, 3035], "postCreateCommand": ".devcontainer/scripts/setup.sh && bundle exec rake db:chatwoot_prepare && yarn", "portsAttributes": { "3000": { "label": "Rails Server" }, + "3035": { + "label": "Webpack Dev Server" + }, "8025": { "label": "Mailhog UI" } - }, + } } diff --git a/.devcontainer/scripts/setup.sh b/.devcontainer/scripts/setup.sh index 1b5842603..32ba272a8 100755 --- a/.devcontainer/scripts/setup.sh +++ b/.devcontainer/scripts/setup.sh @@ -6,3 +6,8 @@ sed -i -e "/FRONTEND_URL/ s/=.*/=https:\/\/$CODESPACE_NAME-3000.githubpreview.de sed -i -e "/WEBPACKER_DEV_SERVER_PUBLIC/ s/=.*/=https:\/\/$CODESPACE_NAME-3035.githubpreview.dev/" .env # uncomment the webpacker env variable sed -i -e '/WEBPACKER_DEV_SERVER_PUBLIC/s/^# //' .env +# fix the error with webpacker +echo 'export NODE_OPTIONS=--openssl-legacy-provider' >> ~/.bashrc + +# codespaces make the ports public +gh codespace ports visibility 3000:public 3035:public 8025:public -c $CODESPACE_NAME From 6c6df8661b2b3bf6e4908465065469aa6fa898a7 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Fri, 17 Jun 2022 19:12:56 +0530 Subject: [PATCH 174/177] chore: Codespace webpacker fix (#4887) - fixes the webpacker error when running overmind start -f Procfile.dev --- .devcontainer/scripts/setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/scripts/setup.sh b/.devcontainer/scripts/setup.sh index 32ba272a8..4ffee2d3a 100755 --- a/.devcontainer/scripts/setup.sh +++ b/.devcontainer/scripts/setup.sh @@ -7,7 +7,7 @@ sed -i -e "/WEBPACKER_DEV_SERVER_PUBLIC/ s/=.*/=https:\/\/$CODESPACE_NAME-3035.g # uncomment the webpacker env variable sed -i -e '/WEBPACKER_DEV_SERVER_PUBLIC/s/^# //' .env # fix the error with webpacker -echo 'export NODE_OPTIONS=--openssl-legacy-provider' >> ~/.bashrc +echo 'export NODE_OPTIONS=--openssl-legacy-provider' >> ~/.zshrc # codespaces make the ports public gh codespace ports visibility 3000:public 3035:public 8025:public -c $CODESPACE_NAME From a8c6cd729b51dfee644da9a8bc1516c53d6f9ea1 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Mon, 20 Jun 2022 14:16:49 +0530 Subject: [PATCH 175/177] chore: Sync pre-chat fields after custom attribute update (#4692) --- ...pdate_widget_pre_chat_custom_fields_job.rb | 21 +++++++++++++ app/models/custom_attribute_definition.rb | 5 ++++ ..._widget_pre_chat_custom_fields_job_spec.rb | 2 +- ..._widget_pre_chat_custom_fields_job_spec.rb | 30 +++++++++++++++++++ 4 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 app/jobs/inboxes/update_widget_pre_chat_custom_fields_job.rb create mode 100644 spec/jobs/inboxes/update_widget_pre_chat_custom_fields_job_spec.rb diff --git a/app/jobs/inboxes/update_widget_pre_chat_custom_fields_job.rb b/app/jobs/inboxes/update_widget_pre_chat_custom_fields_job.rb new file mode 100644 index 000000000..b17ecdf55 --- /dev/null +++ b/app/jobs/inboxes/update_widget_pre_chat_custom_fields_job.rb @@ -0,0 +1,21 @@ +class Inboxes::UpdateWidgetPreChatCustomFieldsJob < ApplicationJob + queue_as :default + + def perform(account, custom_attribute) + attribute_key = custom_attribute['attribute_key'] + account.web_widgets.all.find_each do |web_widget| + pre_chat_fields = web_widget.pre_chat_form_options['pre_chat_fields'] + pre_chat_fields.each_with_index do |pre_chat_field, index| + next unless pre_chat_field['name'] == attribute_key + + web_widget.pre_chat_form_options['pre_chat_fields'][index] = + pre_chat_field.deep_merge({ + 'label' => custom_attribute['attribute_display_name'], + 'placeholder' => custom_attribute['attribute_display_name'], + 'values' => custom_attribute['attribute_values'] + }) + end + web_widget.save! + end + end +end diff --git a/app/models/custom_attribute_definition.rb b/app/models/custom_attribute_definition.rb index fa0370375..59a314da6 100644 --- a/app/models/custom_attribute_definition.rb +++ b/app/models/custom_attribute_definition.rb @@ -34,6 +34,7 @@ class CustomAttributeDefinition < ApplicationRecord enum attribute_display_type: { text: 0, number: 1, currency: 2, percent: 3, link: 4, date: 5, list: 6, checkbox: 7 } belongs_to :account + after_update :update_widget_pre_chat_custom_fields after_destroy :sync_widget_pre_chat_custom_fields private @@ -41,4 +42,8 @@ class CustomAttributeDefinition < ApplicationRecord def sync_widget_pre_chat_custom_fields ::Inboxes::SyncWidgetPreChatCustomFieldsJob.perform_now(account, attribute_key) end + + def update_widget_pre_chat_custom_fields + ::Inboxes::UpdateWidgetPreChatCustomFieldsJob.perform_now(account, self) + end end diff --git a/spec/jobs/inboxes/sync_widget_pre_chat_custom_fields_job_spec.rb b/spec/jobs/inboxes/sync_widget_pre_chat_custom_fields_job_spec.rb index e6b23dccc..9348754d1 100644 --- a/spec/jobs/inboxes/sync_widget_pre_chat_custom_fields_job_spec.rb +++ b/spec/jobs/inboxes/sync_widget_pre_chat_custom_fields_job_spec.rb @@ -15,7 +15,7 @@ RSpec.describe Inboxes::SyncWidgetPreChatCustomFieldsJob, type: :job do end context 'when called' do - it 'reopens snoozed conversations whose snooze until has passed' do + it 'sync pre chat fields if custom attribute deleted' do described_class.perform_now(account, 'developer_id') expect(web_widget.reload.pre_chat_form_options['pre_chat_fields']).to eq [{ 'label' => 'Full Name', diff --git a/spec/jobs/inboxes/update_widget_pre_chat_custom_fields_job_spec.rb b/spec/jobs/inboxes/update_widget_pre_chat_custom_fields_job_spec.rb new file mode 100644 index 000000000..80a32be34 --- /dev/null +++ b/spec/jobs/inboxes/update_widget_pre_chat_custom_fields_job_spec.rb @@ -0,0 +1,30 @@ +require 'rails_helper' + +RSpec.describe Inboxes::UpdateWidgetPreChatCustomFieldsJob, type: :job do + pre_chat_fields = [{ + 'label' => 'Developer Id', + 'name' => 'developer_id' + }, { + 'label' => 'Full Name', + 'name' => 'full_name' + }] + pre_chat_message = 'Share your queries here.' + custom_attribute = { + 'attribute_key' => 'developer_id', + 'attribute_display_name' => 'Developer Number' + } + let!(:account) { create(:account) } + let!(:web_widget) do + create(:channel_widget, account: account, pre_chat_form_options: { pre_chat_message: pre_chat_message, pre_chat_fields: pre_chat_fields }) + end + + context 'when called' do + it 'sync pre chat fields if custom attribute updated' do + described_class.perform_now(account, custom_attribute) + expect(web_widget.reload.pre_chat_form_options['pre_chat_fields']).to eq [ + { 'label' => 'Developer Number', 'name' => 'developer_id', 'placeholder' => 'Developer Number', + 'values' => nil }, { 'label' => 'Full Name', 'name' => 'full_name' } + ] + end + end +end From f31c6d5de285665aff508d15c6121653b274e09d Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Mon, 20 Jun 2022 15:09:22 +0530 Subject: [PATCH 176/177] feat: Update the design of labels to use a `smooth` theme (#4325) Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Pranav Raj S --- .../assets/scss/_foundation-settings.scss | 4 +- .../dashboard/components/ui/Label.vue | 59 ++++++++++++++++--- .../components/widgets/LabelSelector.vue | 3 +- .../conversation/labels/LabelBox.vue | 3 +- .../shared/assets/stylesheets/colors.scss | 10 ++-- .../components/ui/dropdown/AddLabel.vue | 26 ++++---- 6 files changed, 71 insertions(+), 34 deletions(-) diff --git a/app/javascript/dashboard/assets/scss/_foundation-settings.scss b/app/javascript/dashboard/assets/scss/_foundation-settings.scss index c6b7f6fd3..d13dcbe5e 100644 --- a/app/javascript/dashboard/assets/scss/_foundation-settings.scss +++ b/app/javascript/dashboard/assets/scss/_foundation-settings.scss @@ -383,8 +383,8 @@ $form-button-radius: var(--border-radius-normal); // 20. Label // --------- -$label-background: $primary-color; -$label-color: $white; +$label-background: $white; +$label-color: $black; $label-color-alt: $black; $label-palette: $foundation-palette; $label-font-size: $font-size-mini; diff --git a/app/javascript/dashboard/components/ui/Label.vue b/app/javascript/dashboard/components/ui/Label.vue index 504f6fc9e..d4059d21c 100644 --- a/app/javascript/dashboard/components/ui/Label.vue +++ b/app/javascript/dashboard/components/ui/Label.vue @@ -1,13 +1,18 @@