From 9329f08e1408308c1bb798dbd39a8acda0ef7f7a Mon Sep 17 00:00:00 2001 From: Justman10000 <49990629+Justman10000@users.noreply.github.com> Date: Wed, 6 Apr 2022 16:51:19 +0200 Subject: [PATCH 01/53] chore: Fix typo in en localisation file (#4405) Fix Co-authored-by: Vishnu Narayanan --- app/javascript/dashboard/i18n/locale/en/teamsSettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/dashboard/i18n/locale/en/teamsSettings.json b/app/javascript/dashboard/i18n/locale/en/teamsSettings.json index 34d85384b..50dcb780d 100644 --- a/app/javascript/dashboard/i18n/locale/en/teamsSettings.json +++ b/app/javascript/dashboard/i18n/locale/en/teamsSettings.json @@ -83,7 +83,7 @@ "SELECT_ALL": "select all agents", "SELECTED_COUNT": "%{selected} out of %{total} agents selected.", "BUTTON_TEXT": "Add agents", - "AGENT_VALIDATION_ERROR": "Select atleaset one agent." + "AGENT_VALIDATION_ERROR": "Select at least one agent." }, "FINISH": { From 75ce5345a9b20be2402bf1fc83aa92262da79f30 Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Wed, 6 Apr 2022 21:14:04 +0530 Subject: [PATCH 02/53] feat: add gh action to build Chatwoot CE/foss docker image (#4406) Github action to build and push chatwoot-ce(foss) edition images. This action will run on merges to master, develop and when tags are created. Corresponding docker tags are as follows. GitHub branch/tag --> docker tag ---- master --> latest-ce develop ---> develop-ce v2.3.2 ---> v2.3.2-ce v* ---> v*-ce Fixes #4388 --- .github/workflows/publish_foss_docker.yml | 55 +++++++++++++++++++ .../{run-foss-spec.yml => run_foss_spec.yml} | 0 2 files changed, 55 insertions(+) create mode 100644 .github/workflows/publish_foss_docker.yml rename .github/workflows/{run-foss-spec.yml => run_foss_spec.yml} (100%) diff --git a/.github/workflows/publish_foss_docker.yml b/.github/workflows/publish_foss_docker.yml new file mode 100644 index 000000000..f56d67f99 --- /dev/null +++ b/.github/workflows/publish_foss_docker.yml @@ -0,0 +1,55 @@ +# # +# # This action will publish Chatwoot CE docker image. +# # This is set to run against merges to develop, master +# # when tags are created. +# # + +name: Publish Chatwoot CE docker images +on: + push: + branches: + - develop + - master + tags: + - v* + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + env: + GIT_REF: ${{ github.head_ref || github.ref_name }} # ref_name to get tags/branches + steps: + - + name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Strip enterprise code + run: | + rm -rf enterprise + rm -rf spec/enterprise + + - name: set docker tag + run: | + echo "DOCKER_TAG=chatwoot/chatwoot:$GIT_REF-ce" >> $GITHUB_ENV + + - name: replace docker tag if master + if: github.ref_name == 'master' + run: | + echo "DOCKER_TAG=chatwoot/chatwoot:latest-ce" >> $GITHUB_ENV + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v2 + with: + file: docker/Dockerfile + push: true + tags: ${{ env.DOCKER_TAG }} diff --git a/.github/workflows/run-foss-spec.yml b/.github/workflows/run_foss_spec.yml similarity index 100% rename from .github/workflows/run-foss-spec.yml rename to .github/workflows/run_foss_spec.yml From c2afdd5c93cc05b5b90b4c87a875ad3cee886742 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Thu, 7 Apr 2022 00:07:53 +0530 Subject: [PATCH 03/53] chore: Remove unnecessary fields from notification payload (#4403) --- app/models/notification.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/models/notification.rb b/app/models/notification.rb index e0b1ef5ec..78591b96a 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -64,9 +64,15 @@ class Notification < ApplicationRecord def primary_actor_data if %w[assigned_conversation_new_message conversation_mention].include? notification_type - primary_actor.conversation.push_event_data + { + id: primary_actor.conversation.push_event_data[:id], + meta: primary_actor.conversation.push_event_data[:meta] + } else - primary_actor.push_event_data + { + id: primary_actor.push_event_data[:id], + meta: primary_actor.push_event_data[:meta] + } end end From 5b490bc8cc5f284d85bbb601bb20940f968606a1 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 7 Apr 2022 07:14:40 +0530 Subject: [PATCH 04/53] chore: fix scroll bar color in the dark mode (#4404) --- app/javascript/widget/App.vue | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/javascript/widget/App.vue b/app/javascript/widget/App.vue index 9d6600264..f18927f39 100755 --- a/app/javascript/widget/App.vue +++ b/app/javascript/widget/App.vue @@ -302,4 +302,9 @@ export default { From 8482ecc1b10306f86e999f38d2cc7938e059084d Mon Sep 17 00:00:00 2001 From: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com> Date: Thu, 7 Apr 2022 10:01:08 +0530 Subject: [PATCH 05/53] Feat - Sort inboxes alphabetically (#4383) --- .../layout/sidebarComponents/Secondary.vue | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue b/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue index 1f98305a1..8d486a454 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue @@ -85,14 +85,20 @@ export default { toState: frontendURL(`accounts/${this.accountId}/settings/inboxes/new`), toStateName: 'settings_inbox_new', newLinkRouteName: 'settings_inbox_new', - children: this.inboxes.map(inbox => ({ - id: inbox.id, - label: inbox.name, - truncateLabel: true, - toState: frontendURL(`accounts/${this.accountId}/inbox/${inbox.id}`), - type: inbox.channel_type, - phoneNumber: inbox.phone_number, - })), + children: this.inboxes + .map(inbox => ({ + id: inbox.id, + label: inbox.name, + truncateLabel: true, + toState: frontendURL( + `accounts/${this.accountId}/inbox/${inbox.id}` + ), + type: inbox.channel_type, + phoneNumber: inbox.phone_number, + })) + .sort((a, b) => + a.label.toLowerCase() > b.label.toLowerCase() ? 1 : -1 + ), }; }, labelSection() { From d4a046a21ef5de3fb3722b6d72872b2bd05b8eb3 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 7 Apr 2022 10:23:18 +0530 Subject: [PATCH 06/53] fix: Audio notification would not work without action on the dashboard (#4303) * fixes: Audio notification would not work without an action on dashboard * fixes spec * Minor fixes * Minor fixes * Review fixes Co-authored-by: Muhsin Keloth --- app/javascript/shared/helpers/AudioNotificationHelper.js | 6 +++++- .../shared/helpers/specs/AudioNotificationHelper.spec.js | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/javascript/shared/helpers/AudioNotificationHelper.js b/app/javascript/shared/helpers/AudioNotificationHelper.js index 672816f90..d4fbc9e2c 100644 --- a/app/javascript/shared/helpers/AudioNotificationHelper.js +++ b/app/javascript/shared/helpers/AudioNotificationHelper.js @@ -3,7 +3,7 @@ import { IFrameHelper } from 'widget/helpers/utils'; import { showBadgeOnFavicon } from './faviconHelper'; -export const initOnEvents = ['click', 'touchstart', 'keypress']; +export const initOnEvents = ['click', 'touchstart', 'keypress', 'keydown']; export const getAlertAudio = async (baseUrl = '') => { const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const playsound = audioBuffer => { @@ -53,6 +53,10 @@ export const shouldPlayAudio = ( message_type: messageType, private: isPrivate, } = message; + if (!isDocHidden && messageType === MESSAGE_TYPE.INCOMING) { + showBadgeOnFavicon(); + return false; + } const isFromCurrentUser = userId === senderId; const playAudio = diff --git a/app/javascript/shared/helpers/specs/AudioNotificationHelper.spec.js b/app/javascript/shared/helpers/specs/AudioNotificationHelper.spec.js index 7bdb6b059..7dc935400 100644 --- a/app/javascript/shared/helpers/specs/AudioNotificationHelper.spec.js +++ b/app/javascript/shared/helpers/specs/AudioNotificationHelper.spec.js @@ -17,7 +17,7 @@ describe('shouldPlayAudio', () => { message_type: 0, private: false, }; - const [conversationId, userId, isDocHiddden] = [1, 2, false]; + const [conversationId, userId, isDocHiddden] = [1, 2, true]; const result = shouldPlayAudio( message, conversationId, From dfb56f6bb8ecec578cefae06d718331359d073de Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Thu, 7 Apr 2022 19:01:31 +0530 Subject: [PATCH 07/53] doc: Swagger for custom attribute APIs (#4382) --- swagger/definitions/index.yml | 8 +- .../create_update_payload.yml | 22 ++ .../definitions/resource/custom_attribute.yml | 29 ++ swagger/index.yml | 1 + .../application/custom_attributes/create.yml | 20 ++ .../application/custom_attributes/delete.yml | 20 ++ .../application/custom_attributes/index.yml | 24 ++ .../application/custom_attributes/show.yml | 14 + .../application/custom_attributes/update.yml | 28 ++ swagger/paths/index.yml | 18 ++ swagger/swagger.json | 272 +++++++++++++++++- 11 files changed, 453 insertions(+), 3 deletions(-) create mode 100644 swagger/definitions/request/custom_attribute/create_update_payload.yml create mode 100644 swagger/definitions/resource/custom_attribute.yml create mode 100644 swagger/paths/application/custom_attributes/create.yml create mode 100644 swagger/paths/application/custom_attributes/delete.yml create mode 100644 swagger/paths/application/custom_attributes/index.yml create mode 100644 swagger/paths/application/custom_attributes/show.yml create mode 100644 swagger/paths/application/custom_attributes/update.yml diff --git a/swagger/definitions/index.yml b/swagger/definitions/index.yml index c120320c8..c06dd1471 100644 --- a/swagger/definitions/index.yml +++ b/swagger/definitions/index.yml @@ -14,6 +14,8 @@ generic_id: $ref: ./resource/extension/generic.yml canned_response: $ref: ./resource/canned_response.yml +custom_attribute: + $ref: ./resource/custom_attribute.yml contact: $ref: ./resource/contact.yml conversation: @@ -69,6 +71,8 @@ user_create_update_payload: canned_response_create_update_payload: $ref: ./request/canned_response/create_update_payload.yml +custom_attribute_create_update_payload: + $ref: ./request/custom_attribute/create_update_payload.yml ## contact contact_create: $ref: ./request/contact/create.yml @@ -154,6 +158,6 @@ account_summary: $ref: './resource/reports/summary.yml' agent_conversation_metrics: $ref: './resource/reports/conversation/agent.yml' - - + + diff --git a/swagger/definitions/request/custom_attribute/create_update_payload.yml b/swagger/definitions/request/custom_attribute/create_update_payload.yml new file mode 100644 index 000000000..fd368f98c --- /dev/null +++ b/swagger/definitions/request/custom_attribute/create_update_payload.yml @@ -0,0 +1,22 @@ +type: object +properties: + attribute_display_name: + type: string + description: Attribute display name + attribute_display_type: + type: integer + description: Attribute display type (text- 0, number- 1, currency- 2, percent- 3, link- 4, date- 5, list- 6, checkbox- 7) + attribute_description: + type: string + description: Attribute description + attribute_key: + type: string + description: Attribute unique key value + attribute_values: + type: array + description: Attribute values + items: + type: string + attribute_model: + type: integer + description: Attribute type(conversation_attribute- 0, contact_attribute- 1) diff --git a/swagger/definitions/resource/custom_attribute.yml b/swagger/definitions/resource/custom_attribute.yml new file mode 100644 index 000000000..ba36fd714 --- /dev/null +++ b/swagger/definitions/resource/custom_attribute.yml @@ -0,0 +1,29 @@ +type: object +properties: + id: + type: integer + description: Identifier + attribute_display_name: + type: string + description: Attribute display name + attribute_display_type: + type: string + description: Attribute display type (text, number, currency, percent, link, date, list, checkbox) + attribute_description: + type: string + description: Attribute description + attribute_key: + type: string + description: Attribute unique key value + attribute_values: + type: string + description: Attribute values + default_value: + type: string + description: Attribute default value + attribute_model: + type: string + description: Attribute type(conversation_attribute/contact_attribute) + account_id: + type: integer + description: Account Id diff --git a/swagger/index.yml b/swagger/index.yml index 181fed1a8..b2dd91e46 100644 --- a/swagger/index.yml +++ b/swagger/index.yml @@ -66,6 +66,7 @@ x-tagGroups: - Teams - Custom Filters - Reports + - Custom Attributes - name: Client tags: - Contacts API diff --git a/swagger/paths/application/custom_attributes/create.yml b/swagger/paths/application/custom_attributes/create.yml new file mode 100644 index 000000000..fa01fc8e4 --- /dev/null +++ b/swagger/paths/application/custom_attributes/create.yml @@ -0,0 +1,20 @@ +tags: + - Custom Attributes +operationId: add-new-custom-attribute-to-account +summary: Add a new custom attribute +description: Add a new custom attribute to account +security: + - userApiKey: [] +parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/custom_attribute_create_update_payload' +responses: + 200: + description: Success + schema: + $ref: '#/definitions/custom_attribute' + 403: + description: Access denied diff --git a/swagger/paths/application/custom_attributes/delete.yml b/swagger/paths/application/custom_attributes/delete.yml new file mode 100644 index 000000000..cfdb68adc --- /dev/null +++ b/swagger/paths/application/custom_attributes/delete.yml @@ -0,0 +1,20 @@ +tags: + - Custom Attributes +operationId: delete-custom-attribute-from-account +summary: Remove a custom attribute from account +description: Remove a custom attribute from account +security: + - userApiKey: [] +parameters: + - in: path + name: id + type: integer + required: true + description: The ID of the custom attribute to be deleted +responses: + 200: + description: Success + 404: + description: Custom attribute not found + 403: + description: Access denied diff --git a/swagger/paths/application/custom_attributes/index.yml b/swagger/paths/application/custom_attributes/index.yml new file mode 100644 index 000000000..a3a91144b --- /dev/null +++ b/swagger/paths/application/custom_attributes/index.yml @@ -0,0 +1,24 @@ +tags: + - Custom Attributes +operationId: get-account-custom-attribute +summary: List all custom attributes in an account +parameters: + - name: attribute_model + in: query + type: string + enum: ['0', '1'] + description: conversation_attribute(0)/contact_attribute(1) + required: true +description: Get details of custom attributes in an Account +security: + - userApiKey: [] +responses: + 200: + description: Success + schema: + type: array + description: 'Array of all custom attributes' + items: + $ref: '#/definitions/custom_attribute' + 403: + description: Access denied diff --git a/swagger/paths/application/custom_attributes/show.yml b/swagger/paths/application/custom_attributes/show.yml new file mode 100644 index 000000000..4e6f61e49 --- /dev/null +++ b/swagger/paths/application/custom_attributes/show.yml @@ -0,0 +1,14 @@ +tags: + - Custom Attributes +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 +responses: + 200: + description: Success + schema: + $ref: '#/definitions/custom_attribute' + 401: + description: Unauthorized + 404: + description: The given attribute ID does not exist in the account diff --git a/swagger/paths/application/custom_attributes/update.yml b/swagger/paths/application/custom_attributes/update.yml new file mode 100644 index 000000000..4b69f21bd --- /dev/null +++ b/swagger/paths/application/custom_attributes/update.yml @@ -0,0 +1,28 @@ +tags: + - Custom Attributes +operationId: update-custom-attribute-in-account +summary: Update custom attribute in Account +description: Update a custom attribute in account +security: + - userApiKey: [] +parameters: + - in: path + name: id + type: integer + required: true + description: The ID of the custom attribute to be updated. + - name: data + in: body + required: true + schema: + $ref: '#/definitions/custom_attribute_create_update_payload' +responses: + 200: + description: Success + schema: + description: 'The updated custom attribute' + $ref: '#/definitions/custom_attribute' + 404: + description: Agent not found + 403: + description: Access denied diff --git a/swagger/paths/index.yml b/swagger/paths/index.yml index dc00c9b24..87faec643 100644 --- a/swagger/paths/index.yml +++ b/swagger/paths/index.yml @@ -170,6 +170,24 @@ delete: $ref: ./application/canned_responses/delete.yml +# Custom Attributes +/api/v1/accounts/{account_id}/custom_attribute_definitions: + parameters: + - $ref: '#/parameters/account_id' + get: + $ref: ./application/custom_attributes/index.yml + post: + $ref: ./application/custom_attributes/create.yml +/api/v1/accounts/{account_id}/custom_attribute_definitions/{id}: + parameters: + - $ref: '#/parameters/account_id' + get: + $ref: './application/custom_attributes/show.yml' + patch: + $ref: ./application/custom_attributes/update.yml + delete: + $ref: ./application/custom_attributes/delete.yml + # Contacts /api/v1/accounts/{account_id}/contacts: $ref: ./application/contacts/list_create.yml diff --git a/swagger/swagger.json b/swagger/swagger.json index 6e188ad5e..b654a669b 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -1494,6 +1494,202 @@ } } }, + "/api/v1/accounts/{account_id}/custom_attribute_definitions": { + "parameters": [ + { + "$ref": "#/parameters/account_id" + } + ], + "get": { + "tags": [ + "Custom Attributes" + ], + "operationId": "get-account-custom-attribute", + "summary": "List all custom attributes in an account", + "parameters": [ + { + "name": "attribute_model", + "in": "query", + "type": "string", + "enum": [ + "0", + "1" + ], + "description": "conversation_attribute(0)/contact_attribute(1)", + "required": true + } + ], + "description": "Get details of custom attributes in an Account", + "security": [ + { + "userApiKey": [ + + ] + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "type": "array", + "description": "Array of all custom attributes", + "items": { + "$ref": "#/definitions/custom_attribute" + } + } + }, + "403": { + "description": "Access denied" + } + } + }, + "post": { + "tags": [ + "Custom Attributes" + ], + "operationId": "add-new-custom-attribute-to-account", + "summary": "Add a new custom attribute", + "description": "Add a new custom attribute to account", + "security": [ + { + "userApiKey": [ + + ] + } + ], + "parameters": [ + { + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/custom_attribute_create_update_payload" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/custom_attribute" + } + }, + "403": { + "description": "Access denied" + } + } + } + }, + "/api/v1/accounts/{account_id}/custom_attribute_definitions/{id}": { + "parameters": [ + { + "$ref": "#/parameters/account_id" + } + ], + "get": { + "tags": [ + "Custom Attributes" + ], + "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", + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/custom_attribute" + } + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "The given attribute ID does not exist in the account" + } + } + }, + "patch": { + "tags": [ + "Custom Attributes" + ], + "operationId": "update-custom-attribute-in-account", + "summary": "Update custom attribute in Account", + "description": "Update a custom attribute in account", + "security": [ + { + "userApiKey": [ + + ] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "type": "integer", + "required": true, + "description": "The ID of the custom attribute to be updated." + }, + { + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/custom_attribute_create_update_payload" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/custom_attribute" + } + }, + "404": { + "description": "Agent not found" + }, + "403": { + "description": "Access denied" + } + } + }, + "delete": { + "tags": [ + "Custom Attributes" + ], + "operationId": "delete-custom-attribute-from-account", + "summary": "Remove a custom attribute from account", + "description": "Remove a custom attribute from account", + "security": [ + { + "userApiKey": [ + + ] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "type": "integer", + "required": true, + "description": "The ID of the custom attribute to be deleted" + } + ], + "responses": { + "200": { + "description": "Success" + }, + "404": { + "description": "Custom attribute not found" + }, + "403": { + "description": "Access denied" + } + } + } + }, "/api/v1/accounts/{account_id}/contacts": { "get": { "tags": [ @@ -4058,6 +4254,47 @@ } } }, + "custom_attribute": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Identifier" + }, + "attribute_display_name": { + "type": "string", + "description": "Attribute display name" + }, + "attribute_display_type": { + "type": "string", + "description": "Attribute display type (text, number, currency, percent, link, date, list, checkbox)" + }, + "attribute_description": { + "type": "string", + "description": "Attribute description" + }, + "attribute_key": { + "type": "string", + "description": "Attribute unique key value" + }, + "attribute_values": { + "type": "string", + "description": "Attribute values" + }, + "default_value": { + "type": "string", + "description": "Attribute default value" + }, + "attribute_model": { + "type": "string", + "description": "Attribute type(conversation_attribute/contact_attribute)" + }, + "account_id": { + "type": "integer", + "description": "Account Id" + } + } + }, "contact": { "type": "object", "properties": { @@ -4718,6 +4955,38 @@ } } }, + "custom_attribute_create_update_payload": { + "type": "object", + "properties": { + "attribute_display_name": { + "type": "string", + "description": "Attribute display name" + }, + "attribute_display_type": { + "type": "integer", + "description": "Attribute display type (text- 0, number- 1, currency- 2, percent- 3, link- 4, date- 5, list- 6, checkbox- 7)" + }, + "attribute_description": { + "type": "string", + "description": "Attribute description" + }, + "attribute_key": { + "type": "string", + "description": "Attribute unique key value" + }, + "attribute_values": { + "type": "array", + "description": "Attribute values", + "items": { + "type": "string" + } + }, + "attribute_model": { + "type": "integer", + "description": "Attribute type(conversation_attribute- 0, contact_attribute- 1)" + } + } + }, "contact_create": { "type": "object", "required": [ @@ -5449,7 +5718,8 @@ "Profile", "Teams", "Custom Filters", - "Reports" + "Reports", + "Custom Attributes" ] }, { From 5ea043605120677824ff948ea023f03e191473c2 Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Thu, 7 Apr 2022 22:16:45 +0530 Subject: [PATCH 08/53] feat: Adds support for draft in conversation reply box (#4205) * Add draft support * Fixes issue with draft loading * Adds draft for private notes * Use localstorage helper * .remove instead of .clear * Remove timestamp * clearLocalStorageOnLogout * Fix draft save on refresh * Remove usage of delete operator * Adds autosave for draft messages * Remove setinterval and add debounce * Removes draft redundancy check * Adds test cases for debouncer * Update app/javascript/shared/helpers/specs/TimeHelpers.spec.js Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> * Update app/javascript/shared/helpers/specs/TimeHelpers.spec.js Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> * Review fixes * Fixes issue with debouncer * FIxes debouncer issue * Fixes issue with draft empty message * Removes empty keys from local storage drafts * Fixes error with empty draft Co-authored-by: Pranav Raj S Co-authored-by: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: iamsivin --- .../dashboard/components/app/UpdateBanner.vue | 15 ++-- .../components/widgets/WootWriter/Editor.vue | 4 +- .../widgets/conversation/ReplyBox.vue | 82 ++++++++++++++++++- .../dashboard/helper/localStorage.js | 43 ++++++---- .../dashboard/store/modules/auth.js | 9 +- .../store/modules/conversations/helpers.js | 7 ++ .../specs/conversations/helpers.spec.js | 7 ++ app/javascript/dashboard/store/utils/api.js | 6 ++ app/javascript/shared/helpers/TimeHelpers.js | 21 +++++ .../shared/helpers/specs/TimeHelpers.spec.js | 32 ++++++++ 10 files changed, 202 insertions(+), 24 deletions(-) create mode 100644 app/javascript/shared/helpers/TimeHelpers.js create mode 100644 app/javascript/shared/helpers/specs/TimeHelpers.spec.js diff --git a/app/javascript/dashboard/components/app/UpdateBanner.vue b/app/javascript/dashboard/components/app/UpdateBanner.vue index 2e4b00031..63c7bd457 100644 --- a/app/javascript/dashboard/components/app/UpdateBanner.vue +++ b/app/javascript/dashboard/components/app/UpdateBanner.vue @@ -12,12 +12,11 @@ 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 885b76283..87a422be0 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/FilterSelector.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/FilterSelector.vue @@ -61,6 +61,12 @@ @input="handleAgentsFilterSelection" /> +
+ {{ $t('REPORT.BUSINESS_HOURS') }} + + + +
diff --git a/app/javascript/dashboard/store/modules/reports.js b/app/javascript/dashboard/store/modules/reports.js index cb1efe3ff..67f468add 100644 --- a/app/javascript/dashboard/store/modules/reports.js +++ b/app/javascript/dashboard/store/modules/reports.js @@ -42,7 +42,8 @@ export const actions = { reportObj.to, reportObj.type, reportObj.id, - reportObj.groupBy + reportObj.groupBy, + reportObj.businessHours ).then(accountReport => { let { data } = accountReport; data = data.filter( @@ -69,7 +70,8 @@ export const actions = { reportObj.to, reportObj.type, reportObj.id, - reportObj.groupBy + reportObj.groupBy, + reportObj.businessHours ) .then(accountSummary => { commit(types.default.SET_ACCOUNT_SUMMARY, accountSummary.data); diff --git a/app/listeners/reporting_event_listener.rb b/app/listeners/reporting_event_listener.rb index 05aa1db4b..d87b6574b 100644 --- a/app/listeners/reporting_event_listener.rb +++ b/app/listeners/reporting_event_listener.rb @@ -1,4 +1,5 @@ class ReportingEventListener < BaseListener + include ReportingEventHelper def conversation_resolved(event) conversation = extract_conversation_and_account(event)[0] time_to_resolve = conversation.updated_at.to_i - conversation.created_at.to_i @@ -6,10 +7,14 @@ class ReportingEventListener < BaseListener reporting_event = ReportingEvent.new( name: 'conversation_resolved', value: time_to_resolve, + value_in_business_hours: business_hours(conversation.inbox, conversation.created_at, + conversation.updated_at), account_id: conversation.account_id, inbox_id: conversation.inbox_id, user_id: conversation.assignee_id, - conversation_id: conversation.id + conversation_id: conversation.id, + event_start_time: conversation.created_at, + event_end_time: conversation.updated_at ) reporting_event.save end @@ -22,10 +27,14 @@ class ReportingEventListener < BaseListener reporting_event = ReportingEvent.new( name: 'first_response', value: first_response_time, + value_in_business_hours: business_hours(conversation.inbox, conversation.created_at, + message.created_at), account_id: conversation.account_id, inbox_id: conversation.inbox_id, user_id: conversation.assignee_id, - conversation_id: conversation.id + conversation_id: conversation.id, + event_start_time: conversation.created_at, + event_end_time: message.created_at ) reporting_event.save end diff --git a/app/models/reporting_event.rb b/app/models/reporting_event.rb index 288b15504..02f6e4d20 100644 --- a/app/models/reporting_event.rb +++ b/app/models/reporting_event.rb @@ -2,15 +2,18 @@ # # Table name: reporting_events # -# id :bigint not null, primary key -# name :string -# value :float -# created_at :datetime not null -# updated_at :datetime not null -# account_id :integer -# conversation_id :integer -# inbox_id :integer -# user_id :integer +# id :bigint not null, primary key +# event_end_time :datetime +# event_start_time :datetime +# name :string +# value :float +# value_in_business_hours :float +# created_at :datetime not null +# updated_at :datetime not null +# account_id :integer +# conversation_id :integer +# inbox_id :integer +# user_id :integer # # Indexes # diff --git a/db/migrate/20220329131401_add_value_in_business_hours_to_reporting_event.rb b/db/migrate/20220329131401_add_value_in_business_hours_to_reporting_event.rb new file mode 100644 index 000000000..4b8138eb7 --- /dev/null +++ b/db/migrate/20220329131401_add_value_in_business_hours_to_reporting_event.rb @@ -0,0 +1,9 @@ +class AddValueInBusinessHoursToReportingEvent < ActiveRecord::Migration[6.1] + def change + change_table :reporting_events, bulk: true do |t| + t.float :value_in_business_hours, default: nil + t.datetime :event_start_time, default: nil + t.datetime :event_end_time, default: nil + end + end +end diff --git a/db/schema.rb b/db/schema.rb index c99c2b70c..84619b696 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -657,6 +657,9 @@ ActiveRecord::Schema.define(version: 2022_04_05_092033) do t.integer "conversation_id" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false + t.float "value_in_business_hours" + t.datetime "event_start_time" + t.datetime "event_end_time" t.index ["account_id"], name: "index_reporting_events_on_account_id" t.index ["created_at"], name: "index_reporting_events_on_created_at" t.index ["inbox_id"], name: "index_reporting_events_on_inbox_id" diff --git a/spec/listeners/reporting_event_listener_spec.rb b/spec/listeners/reporting_event_listener_spec.rb index 399815014..d3b6bd35d 100644 --- a/spec/listeners/reporting_event_listener_spec.rb +++ b/spec/listeners/reporting_event_listener_spec.rb @@ -17,6 +17,21 @@ describe ReportingEventListener do listener.conversation_resolved(event) expect(account.reporting_events.where(name: 'conversation_resolved').count).to be 1 end + + context 'when business hours enabled for inbox' do + let(:created_at) { Time.zone.parse('March 20, 2022 00:00') } + let(:updated_at) { Time.zone.parse('March 26, 2022 23:59') } + let!(:new_inbox) { create(:inbox, working_hours_enabled: true, account: account) } + let!(:new_conversation) do + create(:conversation, created_at: created_at, updated_at: updated_at, account: account, inbox: new_inbox, assignee: user) + end + + it 'creates conversation_resolved event with business hour value' do + event = Events::Base.new('conversation.resolved', Time.zone.now, conversation: new_conversation) + listener.conversation_resolved(event) + expect(account.reporting_events.where(name: 'conversation_resolved')[0]['value_in_business_hours']).to be 144_000.0 + end + end end describe '#first_reply_created' do @@ -26,5 +41,24 @@ describe ReportingEventListener do listener.first_reply_created(event) expect(account.reporting_events.where(name: 'first_response').count).to eql previous_count + 1 end + + context 'when business hours enabled for inbox' do + let(:conversation_created_at) { Time.zone.parse('March 20, 2022 00:00') } + let(:message_created_at) { Time.zone.parse('March 26, 2022 23:59') } + let!(:new_inbox) { create(:inbox, working_hours_enabled: true, account: account) } + let!(:new_conversation) do + create(:conversation, created_at: conversation_created_at, account: account, inbox: new_inbox, assignee: user) + end + let!(:new_message) do + create(:message, message_type: 'outgoing', created_at: message_created_at, + account: account, inbox: new_inbox, conversation: new_conversation) + end + + it 'creates first_response event with business hour value' do + event = Events::Base.new('first.reply.created', Time.zone.now, message: new_message) + listener.first_reply_created(event) + expect(account.reporting_events.where(name: 'first_response')[0]['value_in_business_hours']).to be 144_000.0 + end + end end end From 727993aa193170eb7d59b35098fd3041f7e2c101 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Fri, 8 Apr 2022 14:02:26 +0530 Subject: [PATCH 12/53] feat: Add toggle button component(#4419) Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> --- .../components/buttons/ToggleButton.vue | 66 +++++++++++++++++++ .../dashboard/settings/automation/Index.vue | 56 ++-------------- 2 files changed, 71 insertions(+), 51 deletions(-) create mode 100644 app/javascript/dashboard/components/buttons/ToggleButton.vue diff --git a/app/javascript/dashboard/components/buttons/ToggleButton.vue b/app/javascript/dashboard/components/buttons/ToggleButton.vue new file mode 100644 index 000000000..8d80ccd94 --- /dev/null +++ b/app/javascript/dashboard/components/buttons/ToggleButton.vue @@ -0,0 +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 811fc95c6..5157c4dfc 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue @@ -34,19 +34,10 @@ {{ automation.name }} {{ automation.description }} - + /> {{ readableTime(automation.created_on) }} @@ -140,11 +131,12 @@ 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() { @@ -238,7 +230,6 @@ export default { mode === 'EDIT' ? this.$t('AUTOMATION.EDIT.API.SUCCESS_MESSAGE') : this.$t('AUTOMATION.ADD.API.SUCCESS_MESSAGE'); - await await this.$store.dispatch(action, payload); this.showAlert(this.$t(successMessage)); this.hideAddPopup(); @@ -290,41 +281,4 @@ export default { .automation__status-checkbox { margin: 0; } -.toggle-button { - background-color: var(--s-200); - position: relative; - display: inline-flex; - height: 19px; - width: 34px; - border: 2px solid transparent; - border-radius: var(--border-radius-large); - cursor: pointer; - transition-property: background-color; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 200ms; - flex-shrink: 0; -} - -.toggle-button.active { - background-color: var(--w-500); -} - -.toggle-button span { - --space-one-point-five: 1.5rem; - height: var(--space-one-point-five); - width: var(--space-one-point-five); - display: inline-block; - background-color: var(--white); - box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px, - rgba(59, 130, 246, 0.5) 0px 0px 0px 0px, rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, - rgba(0, 0, 0, 0.06) 0px 1px 2px 0px; - transform: translate(0, 0); - border-radius: 100%; - transition-property: transform; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 200ms; -} -.toggle-button span.active { - transform: translate(var(--space-one-point-five), var(--space-zero)); -} From 7e5ec7925c35188d0c67cb9ce06afd213aeb5023 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Fri, 8 Apr 2022 14:53:37 +0530 Subject: [PATCH 13/53] feat: Add `phone_number` & `custom_attributes` in create conversation end point (#4421) --- .../api/v1/widget/base_controller.rb | 26 ++++++++++++++++--- .../api/v1/widget/conversations_controller.rb | 11 ++++++-- app/javascript/widget/api/endPoints.js | 2 ++ 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/app/controllers/api/v1/widget/base_controller.rb b/app/controllers/api/v1/widget/base_controller.rb index 8df4737db..67132869d 100644 --- a/app/controllers/api/v1/widget/base_controller.rb +++ b/app/controllers/api/v1/widget/base_controller.rb @@ -39,7 +39,8 @@ class Api::V1::Widget::BaseController < ApplicationController browser: browser_params, referer: permitted_params[:message][:referer_url], initiated_at: timestamp_params - } + }, + custom_attributes: permitted_params[:custom_attributes].presence || {} } end @@ -52,16 +53,33 @@ class Api::V1::Widget::BaseController < ApplicationController mergee_contact: @contact ).perform else - @contact.update!(email: email, name: contact_name) + @contact.update!(email: email) + end + end + + def update_contact_phone_number(phone_number) + contact_with_phone_number = @current_account.contacts.find_by(phone_number: phone_number) + if contact_with_phone_number + @contact = ::ContactMergeAction.new( + account: @current_account, + base_contact: contact_with_phone_number, + mergee_contact: @contact + ).perform + else + @contact.update!(phone_number: phone_number) end end def contact_email - permitted_params[:contact][:email].downcase + permitted_params[:contact][:email].downcase if permitted_params[:contact].present? end def contact_name - params[:contact][:name] || contact_email.split('@')[0] + params[:contact][:name] || contact_email.split('@')[0] if contact_email.present? + end + + def contact_phone_number + params[:contact][:phone_number] end def browser_params diff --git a/app/controllers/api/v1/widget/conversations_controller.rb b/app/controllers/api/v1/widget/conversations_controller.rb index 3b0a460e4..ef2422bef 100644 --- a/app/controllers/api/v1/widget/conversations_controller.rb +++ b/app/controllers/api/v1/widget/conversations_controller.rb @@ -7,12 +7,18 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController def create ActiveRecord::Base.transaction do - update_contact(contact_email) if @contact.email.blank? && contact_email.present? + process_update_contact @conversation = create_conversation conversation.messages.create(message_params) end end + def process_update_contact + update_contact(contact_email) if @contact.email.blank? && contact_email.present? + update_contact_phone_number(contact_phone_number) if @contact.phone_number.blank? && contact_phone_number.present? + @contact.update!(name: contact_name) if contact_name.present? + end + def update_last_seen head :ok && return if conversation.nil? @@ -63,6 +69,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController end def permitted_params - params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id]) + params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id], + custom_attributes: {}) end end diff --git a/app/javascript/widget/api/endPoints.js b/app/javascript/widget/api/endPoints.js index df37f2363..870275382 100755 --- a/app/javascript/widget/api/endPoints.js +++ b/app/javascript/widget/api/endPoints.js @@ -10,12 +10,14 @@ const createConversation = params => { contact: { name: params.fullName, email: params.emailAddress, + phone_number: params.phoneNumber, }, message: { content: params.message, timestamp: new Date().toString(), referer_url: referrerURL, }, + custom_attributes: params.customAttributes, }, }; }; From bc7bcc20b8ff4b1f3a302f13bc028958876d4c4d Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Fri, 8 Apr 2022 15:52:39 +0530 Subject: [PATCH 14/53] Revert "feat: Adds support for draft in conversation reply box (#4205)" (#4425) This reverts commit 5ea043605120677824ff948ea023f03e191473c2. --- .../dashboard/components/app/UpdateBanner.vue | 15 ++-- .../components/widgets/WootWriter/Editor.vue | 4 +- .../widgets/conversation/ReplyBox.vue | 82 +------------------ .../dashboard/helper/localStorage.js | 43 ++++------ .../dashboard/store/modules/auth.js | 9 +- .../store/modules/conversations/helpers.js | 7 -- .../specs/conversations/helpers.spec.js | 7 -- app/javascript/dashboard/store/utils/api.js | 6 -- app/javascript/shared/helpers/TimeHelpers.js | 21 ----- .../shared/helpers/specs/TimeHelpers.spec.js | 32 -------- 10 files changed, 24 insertions(+), 202 deletions(-) delete mode 100644 app/javascript/shared/helpers/TimeHelpers.js delete mode 100644 app/javascript/shared/helpers/specs/TimeHelpers.spec.js diff --git a/app/javascript/dashboard/components/app/UpdateBanner.vue b/app/javascript/dashboard/components/app/UpdateBanner.vue index 63c7bd457..2e4b00031 100644 --- a/app/javascript/dashboard/components/app/UpdateBanner.vue +++ b/app/javascript/dashboard/components/app/UpdateBanner.vue @@ -12,11 +12,12 @@ - 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 16/53] 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 17/53] 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 18/53] 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 19/53] 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 20/53] 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 21/53] 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 22/53] 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 23/53] 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 24/53] 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 25/53] 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 26/53] 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 27/53] 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 28/53] 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 29/53] 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 30/53] 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 31/53] 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 @@