From 9c31d7c672d4f9743ce3a18910d905fe841be7cf Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Wed, 12 Jan 2022 02:55:27 -0800 Subject: [PATCH 001/101] feat: Use vue-router on widget route management (#3415) * feat: Add vue-router to widget Co-authored-by: Pranav * Move to dynamic imports * Move to routerMixin * Fix popup button display * Remove unnecessary import * router -> route * Fix open state * Fix issues * Remove used CSS * Fix specs * Fix specs * Fix widgetColor specs * Fix mutation specs * Fixes broken lint errors * Fixes issues with widget flow Co-authored-by: Nithin Co-authored-by: Nithin David <1277421+nithindavid@users.noreply.github.com> Co-authored-by: Muhsin Keloth --- .../dashboard/components/widgets/Avatar.vue | 4 +- app/javascript/packs/widget.js | 2 + app/javascript/sdk/IFrameHelper.js | 48 ++-- .../shared/components/FluentIcon/icons.json | 1 + app/javascript/widget/App.vue | 188 +++++++-------- app/javascript/widget/assets/scss/woot.scss | 7 - .../widget/components/ChatFooter.vue | 64 +++-- .../widget/components/ChatHeader.vue | 75 +++--- .../widget/components/ChatHeaderExpanded.vue | 20 +- .../widget/components/ChatInputWrap.vue | 10 +- .../widget/components/HeaderActions.vue | 4 +- .../widget/components/PreChat/Form.vue | 35 ++- .../widget/components/TeamAvailability.vue | 10 +- .../widget/components/UnreadMessage.vue | 8 +- .../UnreadMessageList.vue} | 69 ++---- .../components/layouts/ViewWithHeader.vue | 118 ++++++++++ .../widget/constants/widgetBusEvents.js | 3 + app/javascript/widget/helpers/actionCable.js | 5 +- app/javascript/widget/i18n/locale/en.json | 1 + app/javascript/widget/mixins/configMixin.js | 6 +- app/javascript/widget/mixins/routerMixin.js | 10 + app/javascript/widget/router.js | 44 ++-- .../widget/store/modules/appConfig.js | 43 +++- .../widget/store/modules/campaign.js | 28 ++- app/javascript/widget/store/modules/events.js | 14 +- .../modules/specs/agent/mutations.spec.js | 2 +- .../modules/specs/appConfig/actions.spec.js | 6 +- .../modules/specs/appConfig/mutations.spec.js | 2 +- .../modules/specs/campaign/actions.spec.js | 51 +++- .../modules/specs/campaign/getters.spec.js | 13 -- .../modules/specs/campaign/mutations.spec.js | 16 -- app/javascript/widget/store/types.js | 11 +- app/javascript/widget/views/Campaigns.vue | 28 +++ app/javascript/widget/views/Home.vue | 218 ++---------------- app/javascript/widget/views/Messages.vue | 27 +++ app/javascript/widget/views/PreChatForm.vue | 49 ++++ app/javascript/widget/views/Router.vue | 82 ------- .../widget/views/UnreadMessages.vue | 20 ++ 38 files changed, 617 insertions(+), 725 deletions(-) rename app/javascript/widget/{views/Unread.vue => components/UnreadMessageList.vue} (68%) create mode 100644 app/javascript/widget/components/layouts/ViewWithHeader.vue create mode 100644 app/javascript/widget/constants/widgetBusEvents.js create mode 100644 app/javascript/widget/mixins/routerMixin.js create mode 100644 app/javascript/widget/views/Campaigns.vue create mode 100644 app/javascript/widget/views/Messages.vue create mode 100644 app/javascript/widget/views/PreChatForm.vue delete mode 100644 app/javascript/widget/views/Router.vue create mode 100644 app/javascript/widget/views/UnreadMessages.vue diff --git a/app/javascript/dashboard/components/widgets/Avatar.vue b/app/javascript/dashboard/components/widgets/Avatar.vue index 9a409f18b..1d67842a2 100644 --- a/app/javascript/dashboard/components/widgets/Avatar.vue +++ b/app/javascript/dashboard/components/widgets/Avatar.vue @@ -18,11 +18,11 @@ export default { }, backgroundColor: { type: String, - default: 'white', + default: '#c2e1ff', }, color: { type: String, - default: '', + default: '#1976cc', }, customStyle: { type: Object, diff --git a/app/javascript/packs/widget.js b/app/javascript/packs/widget.js index 7aeaf4545..f9f57e2ef 100644 --- a/app/javascript/packs/widget.js +++ b/app/javascript/packs/widget.js @@ -7,6 +7,7 @@ import ActionCableConnector from '../widget/helpers/actionCable'; import { getAlertAudio } from 'shared/helpers/AudioNotificationHelper'; import i18n from '../widget/i18n'; +import router from '../widget/router'; Vue.use(VueI18n); Vue.use(Vuelidate); @@ -22,6 +23,7 @@ Vue.config.productionTip = false; window.onload = () => { window.WOOT_WIDGET = new Vue({ + router, store, i18n: i18nConfig, render: h => h(App), diff --git a/app/javascript/sdk/IFrameHelper.js b/app/javascript/sdk/IFrameHelper.js index ff9bcca91..e10feac6d 100644 --- a/app/javascript/sdk/IFrameHelper.js +++ b/app/javascript/sdk/IFrameHelper.js @@ -150,11 +150,14 @@ export const IFrameHelper = { onBubbleClick(bubbleState); }, + closeWindow: () => { + onBubbleClick({ toggleValue: false }); + removeUnreadClass(); + }, + onBubbleToggle: isOpen => { IFrameHelper.sendMessage('toggle-open', { isOpen }); - if (!isOpen) { - IFrameHelper.events.resetUnreadMode(); - } else { + if (isOpen) { IFrameHelper.pushEvent('webwidget.triggered'); } }, @@ -164,28 +167,6 @@ export const IFrameHelper = { referrerHost, }); }, - - setUnreadMode: message => { - const { unreadMessageCount } = message; - const { isOpen } = window.$chatwoot; - const toggleValue = true; - - if (!isOpen && unreadMessageCount > 0) { - IFrameHelper.sendMessage('set-unread-view'); - onBubbleClick({ toggleValue }); - addUnreadClass(); - } - }, - - setCampaignMode: () => { - const { isOpen } = window.$chatwoot; - const toggleValue = true; - if (!isOpen) { - onBubbleClick({ toggleValue }); - addUnreadClass(); - } - }, - updateIframeHeight: message => { const { extraHeight = 0, isFixedHeight } = message; if (!extraHeight) return; @@ -193,11 +174,12 @@ export const IFrameHelper = { IFrameHelper.setFrameHeightToFitContent(extraHeight, isFixedHeight); }, - resetUnreadMode: () => { - IFrameHelper.sendMessage('unset-unread-view'); - removeUnreadClass(); + setUnreadMode: () => { + addUnreadClass(); + onBubbleClick({ toggleValue: true }); }, + resetUnreadMode: () => removeUnreadClass(), handleNotificationDot: event => { if (window.$chatwoot.hideMessageBubble) { return; @@ -253,14 +235,10 @@ export const IFrameHelper = { } }, toggleCloseButton: () => { + let isMobile = false; if (window.matchMedia('(max-width: 668px)').matches) { - IFrameHelper.sendMessage('toggle-close-button', { - showClose: true, - }); - } else { - IFrameHelper.sendMessage('toggle-close-button', { - showClose: false, - }); + isMobile = true; } + IFrameHelper.sendMessage('toggle-close-button', { isMobile }); }, }; diff --git a/app/javascript/shared/components/FluentIcon/icons.json b/app/javascript/shared/components/FluentIcon/icons.json index f3dc089b7..62798657d 100644 --- a/app/javascript/shared/components/FluentIcon/icons.json +++ b/app/javascript/shared/components/FluentIcon/icons.json @@ -2,6 +2,7 @@ "arrow-clockwise-outline": "M12 4.75a7.25 7.25 0 1 0 7.201 6.406c-.068-.588.358-1.156.95-1.156.515 0 .968.358 1.03.87a9.25 9.25 0 1 1-3.432-6.116V4.25a1 1 0 1 1 2.001 0v2.698l.034.052h-.034v.25a1 1 0 0 1-1 1h-3a1 1 0 1 1 0-2h.666A7.219 7.219 0 0 0 12 4.75Z", "arrow-right-outline": "M13.267 4.209a.75.75 0 0 0-1.034 1.086l6.251 5.955H3.75a.75.75 0 0 0 0 1.5h14.734l-6.251 5.954a.75.75 0 0 0 1.034 1.087l7.42-7.067a.996.996 0 0 0 .3-.58.758.758 0 0 0-.001-.29.995.995 0 0 0-.3-.578l-7.419-7.067Z", "attach-outline": "M11.772 3.743a6 6 0 0 1 8.66 8.302l-.19.197-8.8 8.798-.036.03a3.723 3.723 0 0 1-5.489-4.973.764.764 0 0 1 .085-.13l.054-.06.086-.088.142-.148.002.003 7.436-7.454a.75.75 0 0 1 .977-.074l.084.073a.75.75 0 0 1 .074.976l-.073.084-7.594 7.613a2.23 2.23 0 0 0 3.174 3.106l8.832-8.83A4.502 4.502 0 0 0 13 4.644l-.168.16-.013.014-9.536 9.536a.75.75 0 0 1-1.133-.977l.072-.084 9.549-9.55h.002Z", + "chevron-left-outline": "M15.53 4.22a.75.75 0 0 1 0 1.06L8.81 12l6.72 6.72a.75.75 0 1 1-1.06 1.06l-7.25-7.25a.75.75 0 0 1 0-1.06l7.25-7.25a.75.75 0 0 1 1.06 0Z", "chevron-right-outline": "M8.293 4.293a1 1 0 0 0 0 1.414L14.586 12l-6.293 6.293a1 1 0 1 0 1.414 1.414l7-7a1 1 0 0 0 0-1.414l-7-7a1 1 0 0 0-1.414 0Z", "dismiss-outline": "m4.397 4.554.073-.084a.75.75 0 0 1 .976-.073l.084.073L12 10.939l6.47-6.47a.75.75 0 1 1 1.06 1.061L13.061 12l6.47 6.47a.75.75 0 0 1 .072.976l-.073.084a.75.75 0 0 1-.976.073l-.084-.073L12 13.061l-6.47 6.47a.75.75 0 0 1-1.06-1.061L10.939 12l-6.47-6.47a.75.75 0 0 1-.072-.976l.073-.084-.073.084Z", "document-outline": "M18.5 20a.5.5 0 0 1-.5.5H6a.5.5 0 0 1-.5-.5V4a.5.5 0 0 1 .5-.5h6V8a2 2 0 0 0 2 2h4.5v10Zm-5-15.379L17.378 8.5H14a.5.5 0 0 1-.5-.5V4.621Zm5.914 3.793-5.829-5.828c-.026-.026-.058-.046-.085-.07a2.072 2.072 0 0 0-.219-.18c-.04-.027-.086-.045-.128-.068-.071-.04-.141-.084-.216-.116a1.977 1.977 0 0 0-.624-.138C12.266 2.011 12.22 2 12.172 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9.828a2 2 0 0 0-.586-1.414Z", diff --git a/app/javascript/widget/App.vue b/app/javascript/widget/App.vue index 818ef6db4..6a4d4d7ec 100755 --- a/app/javascript/widget/App.vue +++ b/app/javascript/widget/App.vue @@ -1,61 +1,67 @@ - - diff --git a/app/javascript/widget/components/ChatHeaderExpanded.vue b/app/javascript/widget/components/ChatHeaderExpanded.vue index f80b1b8ab..fc4aab125 100755 --- a/app/javascript/widget/components/ChatHeaderExpanded.vue +++ b/app/javascript/widget/components/ChatHeaderExpanded.vue @@ -1,14 +1,14 @@ @@ -47,6 +49,10 @@ export default { type: Array, default: () => {}, }, + hasConversation: { + type: Boolean, + default: false, + }, }, computed: { ...mapGetters({ widgetColor: 'appConfig/getWidgetColor' }), diff --git a/app/javascript/widget/components/UnreadMessage.vue b/app/javascript/widget/components/UnreadMessage.vue index 48a6b4686..13e8fc250 100644 --- a/app/javascript/widget/components/UnreadMessage.vue +++ b/app/javascript/widget/components/UnreadMessage.vue @@ -21,6 +21,10 @@ import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; import Thumbnail from 'dashboard/components/widgets/Thumbnail'; import configMixin from '../mixins/configMixin'; import { isEmptyObject } from 'widget/helpers/utils'; +import { + ON_CAMPAIGN_MESSAGE_CLICK, + ON_UNREAD_MESSAGE_CLICK, +} from '../constants/widgetBusEvents'; export default { name: 'UnreadMessage', components: { Thumbnail }, @@ -82,9 +86,9 @@ export default { }, onClickMessage() { if (this.campaignId) { - bus.$emit('on-campaign-view-clicked', this.campaignId); + bus.$emit(ON_CAMPAIGN_MESSAGE_CLICK, this.campaignId); } else { - bus.$emit('on-unread-view-clicked'); + bus.$emit(ON_UNREAD_MESSAGE_CLICK); } }, }, diff --git a/app/javascript/widget/views/Unread.vue b/app/javascript/widget/components/UnreadMessageList.vue similarity index 68% rename from app/javascript/widget/views/Unread.vue rename to app/javascript/widget/components/UnreadMessageList.vue index 7eafbdbe2..a3368d4e9 100644 --- a/app/javascript/widget/views/Unread.vue +++ b/app/javascript/widget/components/UnreadMessageList.vue @@ -1,20 +1,16 @@ @@ -12,6 +17,10 @@ export default { props: { message: { type: String, default: '' }, + action: { + type: Object, + default: () => {}, + }, showButton: Boolean, duration: { type: [String, Number], diff --git a/app/javascript/dashboard/components/SnackbarContainer.vue b/app/javascript/dashboard/components/SnackbarContainer.vue index 3ab3d67fc..d0d57370a 100644 --- a/app/javascript/dashboard/components/SnackbarContainer.vue +++ b/app/javascript/dashboard/components/SnackbarContainer.vue @@ -4,6 +4,7 @@ v-for="snackMessage in snackMessages" :key="snackMessage.key" :message="snackMessage.message" + :action="snackMessage.action" /> @@ -35,8 +36,12 @@ export default { bus.$off('newToastMessage', this.onNewToastMessage); }, methods: { - onNewToastMessage(message) { - this.snackMessages.push({ key: new Date().getTime(), message }); + onNewToastMessage(message, action) { + this.snackMessages.push({ + key: new Date().getTime(), + message, + action, + }); window.setTimeout(() => { this.snackMessages.splice(0, 1); }, this.duration); diff --git a/app/javascript/dashboard/components/widgets/conversation/specs/MoreActions.spec.js b/app/javascript/dashboard/components/widgets/conversation/specs/MoreActions.spec.js index 639dc239a..9bf61c59c 100644 --- a/app/javascript/dashboard/components/widgets/conversation/specs/MoreActions.spec.js +++ b/app/javascript/dashboard/components/widgets/conversation/specs/MoreActions.spec.js @@ -84,7 +84,8 @@ describe('MoveActions', () => { expect(window.bus.$emit).toBeCalledWith( 'newToastMessage', - 'This conversation is muted for 6 hours' + 'This conversation is muted for 6 hours', + undefined ); }); }); @@ -109,7 +110,8 @@ describe('MoveActions', () => { expect(window.bus.$emit).toBeCalledWith( 'newToastMessage', - 'This conversation is unmuted' + 'This conversation is unmuted', + undefined ); }); }); diff --git a/app/javascript/dashboard/i18n/locale/en/contact.json b/app/javascript/dashboard/i18n/locale/en/contact.json index baa7c8d7d..976f4c53e 100644 --- a/app/javascript/dashboard/i18n/locale/en/contact.json +++ b/app/javascript/dashboard/i18n/locale/en/contact.json @@ -169,6 +169,7 @@ "SUBMIT": "Send message", "CANCEL": "Cancel", "SUCCESS_MESSAGE": "Message sent!", + "GO_TO_CONVERSATION": "View", "ERROR_MESSAGE": "Couldn't send! try again" } }, diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue index b0251df18..923e6d408 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue @@ -174,7 +174,6 @@ export default { onSuccess() { this.$emit('success'); }, - async handleSubmit() { this.$v.$touch(); if (this.$v.$invalid) { @@ -182,9 +181,17 @@ export default { } try { const payload = this.getNewConversation; - await this.onSubmit(payload); + const data = await this.onSubmit(payload); + const action = { + type: 'link', + to: `/app/accounts/${data.account_id}/conversations/${data.id}`, + message: this.$t('NEW_CONVERSATION.FORM.GO_TO_CONVERSATION'), + }; this.onSuccess(); - this.showAlert(this.$t('NEW_CONVERSATION.FORM.SUCCESS_MESSAGE')); + this.showAlert( + this.$t('NEW_CONVERSATION.FORM.SUCCESS_MESSAGE'), + action + ); } catch (error) { if (error instanceof ExceptionWithMessage) { this.showAlert(error.data); diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/NewConversation.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/NewConversation.vue index c0d342d7a..f8c3b23a3 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/contact/NewConversation.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/NewConversation.vue @@ -49,7 +49,11 @@ export default { this.$emit('cancel'); }, async onSubmit(contactItem) { - await this.$store.dispatch('contactConversations/create', contactItem); + const data = await this.$store.dispatch( + 'contactConversations/create', + contactItem + ); + return data; }, }, }; diff --git a/app/javascript/dashboard/store/modules/contactConversations.js b/app/javascript/dashboard/store/modules/contactConversations.js index 3c3280721..49eeabdc4 100644 --- a/app/javascript/dashboard/store/modules/contactConversations.js +++ b/app/javascript/dashboard/store/modules/contactConversations.js @@ -39,6 +39,7 @@ export const actions = { id: contactId, data, }); + return data; } catch (error) { throw new Error(error); } finally { diff --git a/app/javascript/shared/mixins/alertMixin.js b/app/javascript/shared/mixins/alertMixin.js index ba0064f44..50ace13f1 100644 --- a/app/javascript/shared/mixins/alertMixin.js +++ b/app/javascript/shared/mixins/alertMixin.js @@ -1,7 +1,7 @@ export default { methods: { - showAlert(message) { - bus.$emit('newToastMessage', message); + showAlert(message, action) { + bus.$emit('newToastMessage', message, action); }, }, }; From fcd2b892bf673c3c018c85131eea5be29368d36b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Jan 2022 12:46:09 -0800 Subject: [PATCH 011/101] chore(deps): bump follow-redirects from 1.14.5 to 1.14.7 (#3760) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.5 to 1.14.7. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.5...v1.14.7) --- updated-dependencies: - dependency-name: follow-redirects 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 9fcc42b62..d500d19fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6914,9 +6914,9 @@ flush-write-stream@^1.0.0: readable-stream "^2.3.6" follow-redirects@^1.0.0, follow-redirects@^1.14.0: - version "1.14.5" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.5.tgz#f09a5848981d3c772b5392309778523f8d85c381" - integrity sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA== + version "1.14.7" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685" + integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ== for-in@^1.0.2: version "1.0.2" From 290196d43bca76941297855f3e57260e86d17cca Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Thu, 13 Jan 2022 21:38:10 -0800 Subject: [PATCH 012/101] chore: Clean up assignment logic (#3763) --- app/models/concerns/assignment_handler.rb | 2 +- app/models/notification.rb | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/models/concerns/assignment_handler.rb b/app/models/concerns/assignment_handler.rb index 4fd778d45..76ff47aca 100644 --- a/app/models/concerns/assignment_handler.rb +++ b/app/models/concerns/assignment_handler.rb @@ -4,7 +4,7 @@ module AssignmentHandler included do before_save :ensure_assignee_is_from_team - after_update :notify_assignment_change, :process_assignment_activities + after_commit :notify_assignment_change, :process_assignment_activities end private diff --git a/app/models/notification.rb b/app/models/notification.rb index 1577f8dda..7599d4d6e 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -71,6 +71,7 @@ class Notification < ApplicationRecord end # TODO: move to a data presenter + # rubocop:disable Metrics/CyclomaticComplexity def push_message_title case notification_type when 'conversation_creation' @@ -81,14 +82,15 @@ class Notification < ApplicationRecord I18n.t( 'notifications.notification_title.assigned_conversation_new_message', display_id: conversation.display_id, - content: primary_actor.content&.truncate_words(10) + content: primary_actor&.content&.truncate_words(10) ) when 'conversation_mention' - "[##{conversation.display_id}] #{transform_user_mention_content primary_actor.content}" + "[##{conversation&.display_id}] #{transform_user_mention_content primary_actor&.content}" else '' end end + # rubocop:enable Metrics/CyclomaticComplexity def conversation return primary_actor.conversation if %w[assigned_conversation_new_message conversation_mention].include? notification_type From 4398734bdfd8e59df33a5e75896c8b4aa0062dd9 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Mon, 17 Jan 2022 09:18:54 +0530 Subject: [PATCH 013/101] feat: Adds the ability to have custom view for conversations (#3666) * feat: Adds the ability to save custom filters and display folders on the sidebar * Minor fixes * Review fixes * Review fixes * i18n fixes * Shows conversations when the user click on the folder sidebar item * Spacing fixes * Review fixes Co-authored-by: Muhsin Keloth --- app/javascript/dashboard/api/customViews.js | 14 ++ .../dashboard/components/ChatList.vue | 122 +++++++++++++++--- .../dashboard/components/layout/Sidebar.vue | 3 + .../config/sidebarItems/conversations.js | 2 + .../layout/sidebarComponents/Secondary.vue | 28 ++++ .../sidebarComponents/SecondaryNavItem.vue | 2 +- .../widgets/conversation/ConversationCard.vue | 5 + app/javascript/dashboard/helper/URLHelper.js | 3 + .../i18n/locale/en/advancedFilters.json | 14 ++ .../dashboard/i18n/locale/en/settings.json | 7 +- .../conversation/ConversationView.vue | 5 + .../conversation/conversation.routes.js | 19 +++ .../dashboard/customviews/AddCustomViews.vue | 97 ++++++++++++++ app/javascript/dashboard/store/index.js | 2 + .../dashboard/store/modules/customViews.js | 79 ++++++++++++ .../modules/specs/customViews/actions.spec.js | 72 +++++++++++ .../modules/specs/customViews/fixtures.js | 42 ++++++ .../modules/specs/customViews/getters.spec.js | 65 ++++++++++ .../specs/customViews/mutations.spec.js | 29 +++++ .../dashboard/store/mutation-types.js | 6 + .../FluentIcon/dashboard-icons.json | 1 + 21 files changed, 594 insertions(+), 23 deletions(-) create mode 100644 app/javascript/dashboard/api/customViews.js create mode 100644 app/javascript/dashboard/routes/dashboard/customviews/AddCustomViews.vue create mode 100644 app/javascript/dashboard/store/modules/customViews.js create mode 100644 app/javascript/dashboard/store/modules/specs/customViews/actions.spec.js create mode 100644 app/javascript/dashboard/store/modules/specs/customViews/fixtures.js create mode 100644 app/javascript/dashboard/store/modules/specs/customViews/getters.spec.js create mode 100644 app/javascript/dashboard/store/modules/specs/customViews/mutations.spec.js diff --git a/app/javascript/dashboard/api/customViews.js b/app/javascript/dashboard/api/customViews.js new file mode 100644 index 000000000..bb09047ba --- /dev/null +++ b/app/javascript/dashboard/api/customViews.js @@ -0,0 +1,14 @@ +/* global axios */ +import ApiClient from './ApiClient'; + +class CustomViewsAPI extends ApiClient { + constructor() { + super('custom_filters', { accountScoped: true }); + } + + getCustomViews() { + return axios.get(this.url); + } +} + +export default new CustomViewsAPI(); diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue index bb7858e6c..8affb6066 100644 --- a/app/javascript/dashboard/components/ChatList.vue +++ b/app/javascript/dashboard/components/ChatList.vue @@ -1,26 +1,39 @@ -l diff --git a/app/javascript/shared/mixins/specs/filterFixtures.js b/app/javascript/shared/mixins/specs/filterFixtures.js new file mode 100644 index 000000000..6bacc4456 --- /dev/null +++ b/app/javascript/shared/mixins/specs/filterFixtures.js @@ -0,0 +1,29 @@ +export const filterGroups = [ + { + name: 'Standard Filters', + attributes: [ + { key: 'status', name: 'Status' }, + { key: 'assignee_id', name: 'Assignee Name' }, + { key: 'inbox_id', name: 'Inbox Name' }, + { key: 'team_id', name: 'Team Name' }, + { key: 'display_id', name: 'Conversation Identifier' }, + { key: 'campaign_id', name: 'Campaign Name' }, + { key: 'labels', name: 'Labels' }, + ], + }, + { + name: 'Additional Filters', + attributes: [ + { key: 'browser_language', name: 'Browser Language' }, + { key: 'country_code', name: 'Country Name' }, + { key: 'referer', name: 'Referer link' }, + ], + }, + { + name: 'Custom Attributes', + attributes: [ + { key: 'signed_up_at', name: 'Signed Up At' }, + { key: 'test', name: 'Test' }, + ], + }, +]; diff --git a/app/javascript/shared/mixins/specs/filterMixin.spec.js b/app/javascript/shared/mixins/specs/filterMixin.spec.js new file mode 100644 index 000000000..94fe95ea7 --- /dev/null +++ b/app/javascript/shared/mixins/specs/filterMixin.spec.js @@ -0,0 +1,13 @@ +import filterMixin from '../filterMixin'; +import { shallowMount } from '@vue/test-utils'; +import MockComponent from './MockComponent.vue'; + +describe('Test mixin function', () => { + const wrapper = shallowMount(MockComponent, { + mixins: [filterMixin], + }); + + it('should return proper value from bool', () => { + expect(wrapper.vm.setFilterAttributes).toBeTruthy(); + }); +}); From c0276d252aaa8466faa870fa8aa3678266d2d1ec Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Tue, 1 Feb 2022 16:01:25 +0530 Subject: [PATCH 045/101] chore: add aws rds root cert for tls connection (#3812) * chore: add aws rds cert for tls connection * add aws rds ca cert * remove intermediate ca cert --- config/rds-ca-2019-root.pem | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 config/rds-ca-2019-root.pem diff --git a/config/rds-ca-2019-root.pem b/config/rds-ca-2019-root.pem new file mode 100644 index 000000000..fd4bc861a --- /dev/null +++ b/config/rds-ca-2019-root.pem @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEBjCCAu6gAwIBAgIJAMc0ZzaSUK51MA0GCSqGSIb3DQEBCwUAMIGPMQswCQYD +VQQGEwJVUzEQMA4GA1UEBwwHU2VhdHRsZTETMBEGA1UECAwKV2FzaGluZ3RvbjEi +MCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1h +em9uIFJEUzEgMB4GA1UEAwwXQW1hem9uIFJEUyBSb290IDIwMTkgQ0EwHhcNMTkw +ODIyMTcwODUwWhcNMjQwODIyMTcwODUwWjCBjzELMAkGA1UEBhMCVVMxEDAOBgNV +BAcMB1NlYXR0bGUxEzARBgNVBAgMCldhc2hpbmd0b24xIjAgBgNVBAoMGUFtYXpv +biBXZWIgU2VydmljZXMsIEluYy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxIDAeBgNV +BAMMF0FtYXpvbiBSRFMgUm9vdCAyMDE5IENBMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEArXnF/E6/Qh+ku3hQTSKPMhQQlCpoWvnIthzX6MK3p5a0eXKZ +oWIjYcNNG6UwJjp4fUXl6glp53Jobn+tWNX88dNH2n8DVbppSwScVE2LpuL+94vY +0EYE/XxN7svKea8YvlrqkUBKyxLxTjh+U/KrGOaHxz9v0l6ZNlDbuaZw3qIWdD/I +6aNbGeRUVtpM6P+bWIoxVl/caQylQS6CEYUk+CpVyJSkopwJlzXT07tMoDL5WgX9 +O08KVgDNz9qP/IGtAcRduRcNioH3E9v981QO1zt/Gpb2f8NqAjUUCUZzOnij6mx9 +McZ+9cWX88CRzR0vQODWuZscgI08NvM69Fn2SQIDAQABo2MwYTAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUc19g2LzLA5j0Kxc0LjZa +pmD/vB8wHwYDVR0jBBgwFoAUc19g2LzLA5j0Kxc0LjZapmD/vB8wDQYJKoZIhvcN +AQELBQADggEBAHAG7WTmyjzPRIM85rVj+fWHsLIvqpw6DObIjMWokpliCeMINZFV +ynfgBKsf1ExwbvJNzYFXW6dihnguDG9VMPpi2up/ctQTN8tm9nDKOy08uNZoofMc +NUZxKCEkVKZv+IL4oHoeayt8egtv3ujJM6V14AstMQ6SwvwvA93EP/Ug2e4WAXHu +cbI1NAbUgVDqp+DRdfvZkgYKryjTWd/0+1fS8X1bBZVWzl7eirNVnHbSH2ZDpNuY +0SBd8dj5F6ld3t58ydZbrTHze7JJOd8ijySAp4/kiu9UfZWuTPABzDa/DSdz9Dk/ +zPW4CXXvhLmE02TA9/HeCw3KEHIwicNuEfw= +-----END CERTIFICATE----- From 1f3c5002b34c82327922c0e60b81e47fa1a5c323 Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Tue, 1 Feb 2022 16:34:54 +0530 Subject: [PATCH 046/101] feat: Shows agent avatar for message sent by agent (#3884) Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> --- .../scss/widgets/_conversation-view.scss | 6 +++ .../widgets/conversation/Message.vue | 47 ++++++++++++------- .../i18n/locale/en/conversation.json | 1 + 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss index 1be9e31e0..3066d0e43 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss @@ -228,8 +228,14 @@ @include flex-align(right, null); .wrap { + align-items: flex-end; + display: flex; margin-right: $space-normal; text-align: right; + + .sender--info { + padding: var(--space-small) 0 var(--space-smaller) var(--space-small); + } } .bubble { diff --git a/app/javascript/dashboard/components/widgets/conversation/Message.vue b/app/javascript/dashboard/components/widgets/conversation/Message.vue index a79d4150f..97fda5bcf 100644 --- a/app/javascript/dashboard/components/widgets/conversation/Message.vue +++ b/app/javascript/dashboard/components/widgets/conversation/Message.vue @@ -57,22 +57,26 @@ /> - - - + +
Date: Wed, 2 Feb 2022 18:46:07 +0530 Subject: [PATCH 047/101] feat: Add the ability to add new automation rule (#3459) * Add automation modal * Fix the v-model for automations * Actions and Condition dropdowns for automations * Fix merge conflicts * Handle event change and confirmation * Appends new action * Removes actions * Automation api integration * Api integration for creating automations * Registers vuex module to the global store * Automations table * Updarted labels and actions * Integrate automation api * Fixes the mutation error - removed the data key wrapper * Fixed the automation condition models to work with respective event types * Remove temporary fixes added to the api request * Displa timestamp and automation status values * Added the clone buton * Removed uncessary helper method * Specs for automations * Handle WIP code * Remove the payload wrap * Fix the action query payload * Removed unnecessary files * Disabled Automations routes * Ability to delete automations * Fix specs * Fixed merge conflicts Co-authored-by: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com> Co-authored-by: fayazara Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> --- .codeclimate.yml | 1 + app/javascript/dashboard/api/automation.js | 9 + .../dashboard/api/specs/automation.spec.js | 14 + .../layout/config/sidebarItems/settings.js | 2 +- .../widgets/AutomationActionInput.vue | 182 +++++++ .../components/widgets/FilterInput/Index.vue | 8 +- .../dashboard/helper/actionQueryGenerator.js | 17 + .../helper/specs/actionQueryGenerator.spec.js | 41 ++ .../dashboard/i18n/locale/en/automation.json | 78 ++- .../settings/automation/AddAutomationRule.vue | 446 ++++++++++++++++++ .../dashboard/settings/automation/Index.vue | 202 +++++++- .../settings/automation/automation.routes.js | 2 +- .../settings/automation/constants.js | 229 +++++++++ .../dashboard/settings/settings.routes.js | 2 + app/javascript/dashboard/store/index.js | 2 + .../dashboard/store/modules/automations.js | 89 ++++ .../modules/specs/automations/actions.spec.js | 94 ++++ .../modules/specs/automations/fixtures.js | 64 +++ .../modules/specs/automations/getters.spec.js | 25 + .../specs/automations/mutations.spec.js | 58 +++ .../dashboard/store/mutation-types.js | 7 + .../FluentIcon/dashboard-icons.json | 30 +- app/listeners/automation_rule_listener.rb | 2 +- 23 files changed, 1593 insertions(+), 11 deletions(-) create mode 100644 app/javascript/dashboard/api/automation.js create mode 100644 app/javascript/dashboard/api/specs/automation.spec.js create mode 100644 app/javascript/dashboard/components/widgets/AutomationActionInput.vue create mode 100644 app/javascript/dashboard/helper/actionQueryGenerator.js create mode 100644 app/javascript/dashboard/helper/specs/actionQueryGenerator.spec.js create mode 100644 app/javascript/dashboard/routes/dashboard/settings/automation/AddAutomationRule.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/automation/constants.js create mode 100644 app/javascript/dashboard/store/modules/automations.js create mode 100644 app/javascript/dashboard/store/modules/specs/automations/actions.spec.js create mode 100644 app/javascript/dashboard/store/modules/specs/automations/fixtures.js create mode 100644 app/javascript/dashboard/store/modules/specs/automations/getters.spec.js create mode 100644 app/javascript/dashboard/store/modules/specs/automations/mutations.spec.js diff --git a/.codeclimate.yml b/.codeclimate.yml index 6b9e1a78f..2799086e3 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -47,4 +47,5 @@ exclude_patterns: - 'app/javascript/shared/constants/countries.js' - 'app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/languages.js' - 'app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js' + - 'app/javascript/dashboard/routes/dashboard/settings/automation/constants.js' - 'app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js' diff --git a/app/javascript/dashboard/api/automation.js b/app/javascript/dashboard/api/automation.js new file mode 100644 index 000000000..3c354307d --- /dev/null +++ b/app/javascript/dashboard/api/automation.js @@ -0,0 +1,9 @@ +import ApiClient from './ApiClient'; + +class AutomationsAPI extends ApiClient { + constructor() { + super('automation_rules', { accountScoped: true }); + } +} + +export default new AutomationsAPI(); diff --git a/app/javascript/dashboard/api/specs/automation.spec.js b/app/javascript/dashboard/api/specs/automation.spec.js new file mode 100644 index 000000000..c236b5ce3 --- /dev/null +++ b/app/javascript/dashboard/api/specs/automation.spec.js @@ -0,0 +1,14 @@ +import automations from '../automation'; +import ApiClient from '../ApiClient'; + +describe('#AutomationsAPI', () => { + it('creates correct instance', () => { + expect(automations).toBeInstanceOf(ApiClient); + expect(automations).toHaveProperty('get'); + expect(automations).toHaveProperty('show'); + expect(automations).toHaveProperty('create'); + expect(automations).toHaveProperty('update'); + expect(automations).toHaveProperty('delete'); + expect(automations.url).toBe('/api/v1/automation_rules'); + }); +}); diff --git a/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js b/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js index 3c872f25d..176fd152c 100644 --- a/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js +++ b/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js @@ -70,7 +70,7 @@ const settings = accountId => ({ toStateName: 'attributes_list', }, { - icon: 'autocorrect', + icon: 'automation', label: 'AUTOMATION', hasSubMenu: false, toState: frontendURL(`accounts/${accountId}/settings/automation/list`), diff --git a/app/javascript/dashboard/components/widgets/AutomationActionInput.vue b/app/javascript/dashboard/components/widgets/AutomationActionInput.vue new file mode 100644 index 000000000..22521af91 --- /dev/null +++ b/app/javascript/dashboard/components/widgets/AutomationActionInput.vue @@ -0,0 +1,182 @@ + + + + + diff --git a/app/javascript/dashboard/components/widgets/FilterInput/Index.vue b/app/javascript/dashboard/components/widgets/FilterInput/Index.vue index a64425393..d26467ab6 100644 --- a/app/javascript/dashboard/components/widgets/FilterInput/Index.vue +++ b/app/javascript/dashboard/components/widgets/FilterInput/Index.vue @@ -1,6 +1,6 @@ - + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/automation.routes.js b/app/javascript/dashboard/routes/dashboard/settings/automation/automation.routes.js index 6d751c45a..dbb204261 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/automation/automation.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/automation/automation.routes.js @@ -9,7 +9,7 @@ export default { component: SettingsContent, props: { headerTitle: 'AUTOMATION.HEADER', - icon: 'autocorrect', + icon: 'automation', showNewButton: false, }, children: [ diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js b/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js new file mode 100644 index 000000000..62e4bb998 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js @@ -0,0 +1,229 @@ +const OPERATOR_TYPES_1 = [ + { + value: 'equal_to', + label: 'Equal to', + }, + { + value: 'not_equal_to', + label: 'Not equal to', + }, +]; + +const OPERATOR_TYPES_2 = [ + { + value: 'equal_to', + label: 'Equal to', + }, + { + value: 'not_equal_to', + label: 'Not equal to', + }, + { + value: 'contains', + label: 'Contains', + }, + { + value: 'does_not_contain', + label: 'Does not contain', + }, +]; + +const OPERATOR_TYPES_3 = [ + { + value: 'equal_to', + label: 'Equal to', + }, + { + value: 'not_equal_to', + label: 'Not equal to', + }, + { + value: 'is_present', + label: 'Is present', + }, + { + value: 'is_not_present', + label: 'Is not present', + }, +]; + +export const AUTOMATIONS = { + message_created: { + conditions: [ + { + key: 'message_type', + name: 'Message Type', + attributeI18nKey: 'MESSAGE_TYPE', + inputType: 'search_select', + filterOperators: OPERATOR_TYPES_1, + }, + { + key: 'message_contains', + name: 'Message Contains', + attributeI18nKey: 'MESSAGE_CONTAINS', + inputType: 'plain_text', + filterOperators: OPERATOR_TYPES_2, + }, + ], + actions: [ + { + key: 'assign_team', + name: 'Assign a team', + attributeI18nKey: 'ASSIGN_TEAM', + }, + { + key: 'add_label', + name: 'Add a label', + attributeI18nKey: 'ADD_LABEL', + }, + { + key: 'send_message', + name: 'Send an email to team', + attributeI18nKey: 'SEND_MESSAGE', + }, + ], + }, + conversation_created: { + conditions: [ + { + key: 'status', + name: 'Status', + attributeI18nKey: 'STATUS', + inputType: 'multi_select', + filterOperators: OPERATOR_TYPES_1, + }, + { + key: 'browser_language', + name: 'Browser Language', + attributeI18nKey: 'BROWSER_LANGUAGE', + inputType: 'search_select', + filterOperators: OPERATOR_TYPES_1, + }, + { + key: 'country_code', + name: 'Country', + attributeI18nKey: 'COUNTRY_NAME', + inputType: 'search_select', + filterOperators: OPERATOR_TYPES_1, + }, + { + key: 'referrer', + name: 'Referrer Link', + attributeI18nKey: 'REFERER_LINK', + inputType: 'plain_text', + filterOperators: OPERATOR_TYPES_2, + }, + ], + actions: [ + { + key: 'assign_team', + name: 'Assign a team', + attributeI18nKey: 'ASSIGN_TEAM', + }, + { + key: 'send_message', + name: 'Send an email to team', + attributeI18nKey: 'SEND_MESSAGE', + }, + { + key: 'assign_agent', + name: 'Assign an agent', + attributeI18nKey: 'ASSIGN_AGENT', + }, + ], + }, + conversation_updated: { + conditions: [ + { + key: 'status', + name: 'Status', + attributeI18nKey: 'STATUS', + inputType: 'multi_select', + filterOperators: OPERATOR_TYPES_1, + }, + { + key: 'browser_language', + name: 'Browser Language', + attributeI18nKey: 'BROWSER_LANGUAGE', + inputType: 'search_select', + filterOperators: OPERATOR_TYPES_1, + }, + { + key: 'country_code', + name: 'Country', + attributeI18nKey: 'COUNTRY_NAME', + inputType: 'search_select', + filterOperators: OPERATOR_TYPES_1, + }, + { + key: 'referer', + name: 'Referrer Link', + attributeI18nKey: 'REFERER_LINK', + inputType: 'plain_text', + filterOperators: OPERATOR_TYPES_2, + }, + { + key: 'assignee_id', + name: 'Assignee', + attributeI18nKey: 'ASSIGNEE_NAME', + inputType: 'search_select', + filterOperators: OPERATOR_TYPES_3, + }, + { + key: 'team_id', + name: 'Team', + attributeI18nKey: 'TEAM_NAME', + inputType: 'search_select', + filterOperators: OPERATOR_TYPES_3, + }, + ], + actions: [ + { + key: 'assign_team', + name: 'Assign a team', + attributeI18nKey: 'ASSIGN_TEAM', + }, + { + key: 'send_message', + name: 'Send an email to team', + attributeI18nKey: 'SEND_MESSAGE', + }, + { + key: 'assign_agent', + name: 'Assign an agent', + attributeI18nKey: 'ASSIGN_AGENT', + attributeKey: 'assignee_id', + }, + ], + }, +}; + +export const AUTOMATION_RULE_EVENTS = [ + { + key: 'conversation_created', + value: 'Conversation Created', + }, + { + key: 'conversation_updated', + value: 'Conversation Updated', + }, + { + key: 'message_created', + value: 'Message Created', + }, +]; + +export const AUTOMATION_ACTION_TYPES = [ + { + key: 'assign_team', + label: 'Assign a team', + }, + { + key: 'add_label', + label: 'Add a label', + }, + { + key: 'send_message', + label: 'Send an email to team', + }, +]; diff --git a/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js b/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js index 87910efc8..110f26fba 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js @@ -11,6 +11,7 @@ import reports from './reports/reports.routes'; import campaigns from './campaigns/campaigns.routes'; import teams from './teams/teams.routes'; import attributes from './attributes/attributes.routes'; +import automation from './automation/automation.routes'; import store from '../../../store'; export default { @@ -38,5 +39,6 @@ export default { ...campaigns.routes, ...integrationapps.routes, ...attributes.routes, + ...automation.routes, ], }; diff --git a/app/javascript/dashboard/store/index.js b/app/javascript/dashboard/store/index.js index 77c62848a..88c36cdf8 100755 --- a/app/javascript/dashboard/store/index.js +++ b/app/javascript/dashboard/store/index.js @@ -31,6 +31,7 @@ import teams from './modules/teams'; import userNotificationSettings from './modules/userNotificationSettings'; import webhooks from './modules/webhooks'; import attributes from './modules/attributes'; +import automations from './modules/automations'; import customViews from './modules/customViews'; Vue.use(Vuex); @@ -66,6 +67,7 @@ export default new Vuex.Store({ userNotificationSettings, webhooks, attributes, + automations, customViews, }, }); diff --git a/app/javascript/dashboard/store/modules/automations.js b/app/javascript/dashboard/store/modules/automations.js new file mode 100644 index 000000000..405118707 --- /dev/null +++ b/app/javascript/dashboard/store/modules/automations.js @@ -0,0 +1,89 @@ +import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers'; +import types from '../mutation-types'; +import AutomationAPI from '../../api/automation'; + +export const state = { + records: [], + uiFlags: { + isFetching: false, + isCreating: false, + isDeleting: false, + }, +}; + +export const getters = { + getAutomations(_state) { + return _state.records; + }, + getUIFlags(_state) { + return _state.uiFlags; + }, +}; + +export const actions = { + get: async function getAutomations({ commit }) { + commit(types.SET_AUTOMATION_UI_FLAG, { isFetching: true }); + try { + const response = await AutomationAPI.get(); + commit(types.SET_AUTOMATIONS, response.data.payload); + } catch (error) { + // Ignore error + } finally { + commit(types.SET_AUTOMATION_UI_FLAG, { isFetching: false }); + } + }, + create: async function createAutomation({ commit }, automationObj) { + commit(types.SET_AUTOMATION_UI_FLAG, { isCreating: true }); + try { + const response = await AutomationAPI.create(automationObj); + commit(types.ADD_AUTOMATION, response.data); + } catch (error) { + throw new Error(error); + } finally { + commit(types.SET_AUTOMATION_UI_FLAG, { isCreating: false }); + } + }, + update: async ({ commit }, { id, ...updateObj }) => { + commit(types.SET_AUTOMATION_UI_FLAG, { isUpdating: true }); + try { + const response = await AutomationAPI.update(id, updateObj); + commit(types.EDIT_AUTOMATION, response.data); + } catch (error) { + throw new Error(error); + } finally { + commit(types.SET_AUTOMATION_UI_FLAG, { isUpdating: false }); + } + }, + delete: async ({ commit }, id) => { + commit(types.SET_AUTOMATION_UI_FLAG, { isDeleting: true }); + try { + await AutomationAPI.delete(id); + commit(types.DELETE_AUTOMATION, id); + } catch (error) { + throw new Error(error); + } finally { + commit(types.SET_AUTOMATION_UI_FLAG, { isDeleting: false }); + } + }, +}; + +export const mutations = { + [types.SET_AUTOMATION_UI_FLAG](_state, data) { + _state.uiFlags = { + ..._state.uiFlags, + ...data, + }; + }, + [types.ADD_AUTOMATION]: MutationHelpers.create, + [types.SET_AUTOMATIONS]: MutationHelpers.set, + // [types.EDIT_AUTOMATION]: MutationHelpers.update, + [types.DELETE_AUTOMATION]: MutationHelpers.destroy, +}; + +export default { + namespaced: true, + actions, + state, + getters, + mutations, +}; diff --git a/app/javascript/dashboard/store/modules/specs/automations/actions.spec.js b/app/javascript/dashboard/store/modules/specs/automations/actions.spec.js new file mode 100644 index 000000000..8dbc08f00 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/automations/actions.spec.js @@ -0,0 +1,94 @@ +import axios from 'axios'; +import { actions } from '../../automations'; +import * as types from '../../../mutation-types'; +import automationsList from './fixtures'; + +const commit = jest.fn(); +global.axios = axios; +jest.mock('axios'); + +describe('#actions', () => { + describe('#get', () => { + it('sends correct actions if API is success', async () => { + axios.get.mockResolvedValue({ data: { payload: automationsList } }); + await actions.get({ commit }); + expect(commit.mock.calls).toEqual([ + [types.default.SET_AUTOMATION_UI_FLAG, { isFetching: true }], + [types.default.SET_AUTOMATIONS, automationsList], + [types.default.SET_AUTOMATION_UI_FLAG, { isFetching: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.get.mockRejectedValue({ message: 'Incorrect header' }); + await actions.get({ commit }); + expect(commit.mock.calls).toEqual([ + [types.default.SET_AUTOMATION_UI_FLAG, { isFetching: true }], + [types.default.SET_AUTOMATION_UI_FLAG, { isFetching: false }], + ]); + }); + }); + + describe('#create', () => { + it('sends correct actions if API is success', async () => { + axios.post.mockResolvedValue({ data: automationsList[0] }); + await actions.create({ commit }, automationsList[0]); + expect(commit.mock.calls).toEqual([ + [types.default.SET_AUTOMATION_UI_FLAG, { isCreating: true }], + [types.default.ADD_AUTOMATION, automationsList[0]], + [types.default.SET_AUTOMATION_UI_FLAG, { isCreating: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.post.mockRejectedValue({ message: 'Incorrect header' }); + await expect(actions.create({ commit })).rejects.toThrow(Error); + expect(commit.mock.calls).toEqual([ + [types.default.SET_AUTOMATION_UI_FLAG, { isCreating: true }], + [types.default.SET_AUTOMATION_UI_FLAG, { isCreating: false }], + ]); + }); + }); + // API Work in progress + // describe('#update', () => { + // it('sends correct actions if API is success', async () => { + // axios.patch.mockResolvedValue({ data: automationsList[0] }); + // await actions.update({ commit }, automationsList[0]); + // expect(commit.mock.calls).toEqual([ + // [types.default.SET_AUTOMATION_UI_FLAG, { isUpdating: true }], + // [types.default.EDIT_AUTOMATION, automationsList[0]], + // [types.default.SET_AUTOMATION_UI_FLAG, { isUpdating: false }], + // ]); + // }); + // it('sends correct actions if API is error', async () => { + // axios.patch.mockRejectedValue({ message: 'Incorrect header' }); + // await expect( + // actions.update({ commit }, automationsList[0]) + // ).rejects.toThrow(Error); + // expect(commit.mock.calls).toEqual([ + // [types.default.SET_AUTOMATION_UI_FLAG, { isUpdating: true }], + // [types.default.SET_AUTOMATION_UI_FLAG, { isUpdating: false }], + // ]); + // }); + // }); + + describe('#delete', () => { + it('sends correct actions if API is success', async () => { + axios.delete.mockResolvedValue({ data: automationsList[0] }); + await actions.delete({ commit }, automationsList[0].id); + expect(commit.mock.calls).toEqual([ + [types.default.SET_AUTOMATION_UI_FLAG, { isDeleting: true }], + [types.default.DELETE_AUTOMATION, automationsList[0].id], + [types.default.SET_AUTOMATION_UI_FLAG, { isDeleting: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.delete.mockRejectedValue({ message: 'Incorrect header' }); + await expect( + actions.delete({ commit }, automationsList[0].id) + ).rejects.toThrow(Error); + expect(commit.mock.calls).toEqual([ + [types.default.SET_AUTOMATION_UI_FLAG, { isDeleting: true }], + [types.default.SET_AUTOMATION_UI_FLAG, { isDeleting: false }], + ]); + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/automations/fixtures.js b/app/javascript/dashboard/store/modules/specs/automations/fixtures.js new file mode 100644 index 000000000..65b55aa73 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/automations/fixtures.js @@ -0,0 +1,64 @@ +export default [ + { + id: 12, + account_id: 1, + name: 'Test', + description: 'This is a test', + event_name: 'conversation_created', + conditions: [ + { + values: ['open'], + attribute_key: 'status', + query_operator: null, + filter_operator: 'equal_to', + }, + ], + actions: [ + { + action_name: 'add_label', + action_params: [{}], + }, + ], + created_on: '2022-01-14T09:17:55.689Z', + active: true, + }, + { + id: 13, + account_id: 1, + name: 'Auto resolve conversation', + description: 'Auto resolves conversation', + event_name: 'conversation_updated', + conditions: [ + { + values: ['resolved'], + attribute_key: 'status', + query_operator: null, + filter_operator: 'equal_to', + }, + ], + actions: [ + { + action_name: 'add_label', + action_params: [{}], + }, + ], + created_on: '2022-01-14T13:06:31.843Z', + active: true, + }, + { + id: 14, + account_id: 1, + name: 'Fayaz', + description: 'This is a test', + event_name: 'conversation_created', + conditions: {}, + actions: [ + { + action_name: 'add_label', + action_params: [{}], + }, + ], + created_on: '2022-01-17T06:46:08.098Z', + active: true, + }, +]; diff --git a/app/javascript/dashboard/store/modules/specs/automations/getters.spec.js b/app/javascript/dashboard/store/modules/specs/automations/getters.spec.js new file mode 100644 index 000000000..46814f02a --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/automations/getters.spec.js @@ -0,0 +1,25 @@ +import { getters } from '../../automations'; +import automations from './fixtures'; +describe('#getters', () => { + it('getAutomations', () => { + const state = { records: automations }; + expect(getters.getAutomations(state)).toEqual(automations); + }); + + it('getUIFlags', () => { + const state = { + uiFlags: { + isFetching: true, + isCreating: false, + isUpdating: false, + isDeleting: false, + }, + }; + expect(getters.getUIFlags(state)).toEqual({ + isFetching: true, + isCreating: false, + isUpdating: false, + isDeleting: false, + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/automations/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/automations/mutations.spec.js new file mode 100644 index 000000000..bdb127d0c --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/automations/mutations.spec.js @@ -0,0 +1,58 @@ +import types from '../../../mutation-types'; +import { mutations } from '../../automations'; +import automations from './fixtures'; +describe('#mutations', () => { + describe('#SET_automations', () => { + it('set autonmation records', () => { + const state = { records: [] }; + mutations[types.SET_AUTOMATIONS](state, automations); + expect(state.records).toEqual(automations); + }); + }); + + describe('#ADD_AUTOMATION', () => { + it('push newly created automatuion to the store', () => { + const state = { records: [automations[0]] }; + mutations[types.ADD_AUTOMATION](state, automations[1]); + expect(state.records).toEqual([automations[0], automations[1]]); + }); + }); + + // describe('#EDIT_AUTOMATION', () => { + // it('update automation record', () => { + // const state = { records: [automations[0]] }; + // mutations[types.EDIT_AUTOMATION](state, { + // id: 12, + // account_id: 1, + // name: 'Test Automation', + // description: 'This is a test', + // event_name: 'conversation_created', + // conditions: [ + // { + // values: ['open'], + // attribute_key: 'status', + // query_operator: null, + // filter_operator: 'equal_to', + // }, + // ], + // actions: [ + // { + // action_name: 'add_label', + // action_params: [{}], + // }, + // ], + // created_on: '2022-01-14T09:17:55.689Z', + // active: true, + // }); + // expect(state.records[0].name).toEqual('Test Automation'); + // }); + // }); + + describe('#DELETE_AUTOMATION', () => { + it('delete automation record', () => { + const state = { records: [automations[0]] }; + mutations[types.DELETE_AUTOMATION](state, 12); + expect(state.records).toEqual([]); + }); + }); +}); diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index e99412c60..734e0b5e5 100755 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -189,6 +189,13 @@ export default { EDIT_CUSTOM_ATTRIBUTE: 'EDIT_CUSTOM_ATTRIBUTE', DELETE_CUSTOM_ATTRIBUTE: 'DELETE_CUSTOM_ATTRIBUTE', + // Automations + SET_AUTOMATION_UI_FLAG: 'SET_AUTOMATION_UI_FLAG', + SET_AUTOMATIONS: 'SET_AUTOMATIONS', + ADD_AUTOMATION: 'ADD_AUTOMATION', + EDIT_AUTOMATION: 'EDIT_AUTOMATION', + DELETE_AUTOMATION: 'DELETE_AUTOMATION', + // Custom Views SET_CUSTOM_VIEW_UI_FLAG: 'SET_CUSTOM_VIEW_UI_FLAG', SET_CUSTOM_VIEW: 'SET_CUSTOM_VIEW', diff --git a/app/javascript/shared/components/FluentIcon/dashboard-icons.json b/app/javascript/shared/components/FluentIcon/dashboard-icons.json index a8840937c..46bb92a22 100644 --- a/app/javascript/shared/components/FluentIcon/dashboard-icons.json +++ b/app/javascript/shared/components/FluentIcon/dashboard-icons.json @@ -14,16 +14,28 @@ "arrow-up-outline": "M4.21 10.733a.75.75 0 0 0 1.086 1.034l5.954-6.251V20.25a.75.75 0 0 0 1.5 0V5.516l5.955 6.251a.75.75 0 0 0 1.086-1.034l-7.067-7.42a.995.995 0 0 0-.58-.3.754.754 0 0 0-.29.001.995.995 0 0 0-.578.3L4.21 10.733Z", "attach-outline": "M11.772 3.743a6 6 0 0 1 8.66 8.302l-.19.197-8.8 8.798-.036.03a3.723 3.723 0 0 1-5.489-4.973.764.764 0 0 1 .085-.13l.054-.06.086-.088.142-.148.002.003 7.436-7.454a.75.75 0 0 1 .977-.074l.084.073a.75.75 0 0 1 .074.976l-.073.084-7.594 7.613a2.23 2.23 0 0 0 3.174 3.106l8.832-8.83A4.502 4.502 0 0 0 13 4.644l-.168.16-.013.014-9.536 9.536a.75.75 0 0 1-1.133-.977l.072-.084 9.549-9.55h.002Z", "autocorrect-outline": "M13.461 4.934c.293.184.548.42.752.698l.117.171 2.945 4.696H21.5a.75.75 0 0 1 .743.649l.007.102a.75.75 0 0 1-.75.75l-3.284-.001.006.009-.009-.01a4.75 4.75 0 1 1-3.463-1.5h.756L13.059 6.6a1.25 1.25 0 0 0-2.04-.112l-.078.112-7.556 12.048a.75.75 0 0 1-1.322-.699l.052-.098L9.67 5.803a2.75 2.75 0 0 1 3.791-.869ZM14.751 12a3.25 3.25 0 1 0 0 6.5 3.25 3.25 0 0 0 0-6.5Z", + "automation-outline": [ + "M21.78 3.28a.75.75 0 0 0-1.06-1.06l-2.012 2.012a4.251 4.251 0 0 0-5.463.462l-1.068 1.069a1.75 1.75 0 0 0 0 2.474l3.585 3.586a1.75 1.75 0 0 0 2.475 0l1.068-1.068a4.251 4.251 0 0 0 .463-5.463L21.78 3.28zm-3.585 2.475l.022.023l.003.002l.002.003l.023.022a2.75 2.75 0 0 1 0 3.89l-1.068 1.067a.25.25 0 0 1-.354 0l-3.586-3.585a.25.25 0 0 1 0-.354l1.068-1.068a2.75 2.75 0 0 1 3.89 0z", + "M10.78 11.28a.75.75 0 1 0-1.06-1.06L8 11.94l-.47-.47a.75.75 0 0 0-1.06 0l-1.775 1.775a4.251 4.251 0 0 0-.463 5.463L2.22 20.72a.75.75 0 1 0 1.06 1.06l2.012-2.012a4.251 4.251 0 0 0 5.463-.463l1.775-1.775a.75.75 0 0 0 0-1.06l-.47-.47l1.72-1.72a.75.75 0 1 0-1.06-1.06L11 14.94L9.06 13l1.72-1.72zm-3.314 2.247l.004.003l.003.004l2.993 2.993l.004.003l.003.004l.466.466l-1.244 1.245a2.75 2.75 0 0 1-3.89 0l-.05-.05a2.75 2.75 0 0 1 0-3.89L7 13.062l.466.466z" + ], "book-contacts-outline": "M15.5 12.25a.75.75 0 0 0-.75-.75h-5a.75.75 0 0 0-.75.75v.5c0 1 1.383 1.75 3.25 1.75s3.25-.75 3.25-1.75v-.5ZM14 8.745C14 7.78 13.217 7 12.25 7s-1.75.779-1.75 1.745a1.75 1.75 0 1 0 3.5 0ZM4 4.5A2.5 2.5 0 0 1 6.5 2H18a2.5 2.5 0 0 1 2.5 2.5v14.25a.75.75 0 0 1-.75.75H5.5a1 1 0 0 0 1 1h13.25a.75.75 0 0 1 0 1.5H6.5A2.5 2.5 0 0 1 4 19.5v-15Zm1.5 0V18H19V4.5a1 1 0 0 0-1-1H6.5a1 1 0 0 0-1 1Z", - "book-clock-outline": ["M13 9.125v1.625h.75a.625.625 0 1 1 0 1.25H12.5a.615.615 0 0 1-.063-.003.625.625 0 0 1-.688-.622v-2.25a.625.625 0 1 1 1.251 0Z", "M12.375 6.005a4.75 4.75 0 1 0 0 9.5 4.75 4.75 0 0 0 0-9.5Zm-3.5 4.75a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0Z", "M6.5 2A2.5 2.5 0 0 0 4 4.5v15A2.5 2.5 0 0 0 6.5 22h13.25a.75.75 0 0 0 0-1.5H6.5a1 1 0 0 1-1-1h14.25a.75.75 0 0 0 .75-.75V4.5A2.5 2.5 0 0 0 18 2H6.5ZM19 18H5.5V4.5a1 1 0 0 1 1-1H18a1 1 0 0 1 1 1V18Z"], + "book-clock-outline": [ + "M13 9.125v1.625h.75a.625.625 0 1 1 0 1.25H12.5a.615.615 0 0 1-.063-.003.625.625 0 0 1-.688-.622v-2.25a.625.625 0 1 1 1.251 0Z", + "M12.375 6.005a4.75 4.75 0 1 0 0 9.5 4.75 4.75 0 0 0 0-9.5Zm-3.5 4.75a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0Z", + "M6.5 2A2.5 2.5 0 0 0 4 4.5v15A2.5 2.5 0 0 0 6.5 22h13.25a.75.75 0 0 0 0-1.5H6.5a1 1 0 0 1-1-1h14.25a.75.75 0 0 0 .75-.75V4.5A2.5 2.5 0 0 0 18 2H6.5ZM19 18H5.5V4.5a1 1 0 0 1 1-1H18a1 1 0 0 1 1 1V18Z" + ], "building-bank-outline": "M13.032 2.325a1.75 1.75 0 0 0-2.064 0L3.547 7.74c-.978.713-.473 2.26.736 2.26H4.5v5.8A2.75 2.75 0 0 0 3 18.25v1.5c0 .413.336.75.75.75h16.5a.75.75 0 0 0 .75-.75v-1.5a2.75 2.75 0 0 0-1.5-2.45V10h.217c1.21 0 1.713-1.547.736-2.26l-7.421-5.416Zm-1.18 1.211a.25.25 0 0 1 .295 0L18.95 8.5H5.05l6.803-4.964ZM18 10v5.5h-2V10h2Zm-3.5 0v5.5h-1.75V10h1.75Zm-3.25 0v5.5H9.5V10h1.75Zm-5.5 7h12.5c.69 0 1.25.56 1.25 1.25V19h-15v-.75c0-.69.56-1.25 1.25-1.25ZM6 15.5V10h2v5.5H6Z", - "calendar-clock-outline": ["M21 6.25A3.25 3.25 0 0 0 17.75 3H6.25A3.25 3.25 0 0 0 3 6.25v11.5A3.25 3.25 0 0 0 6.25 21h5.772a6.471 6.471 0 0 1-.709-1.5H6.25a1.75 1.75 0 0 1-1.75-1.75V8.5h15v2.813a6.471 6.471 0 0 1 1.5.709V6.25ZM6.25 4.5h11.5c.966 0 1.75.784 1.75 1.75V7h-15v-.75c0-.966.784-1.75 1.75-1.75Z", "M23 17.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0Zm-5.5 0h2a.5.5 0 0 1 0 1H17a.5.5 0 0 1-.5-.491v-3.01a.5.5 0 0 1 1 0V17.5Z"], + "calendar-clock-outline": [ + "M21 6.25A3.25 3.25 0 0 0 17.75 3H6.25A3.25 3.25 0 0 0 3 6.25v11.5A3.25 3.25 0 0 0 6.25 21h5.772a6.471 6.471 0 0 1-.709-1.5H6.25a1.75 1.75 0 0 1-1.75-1.75V8.5h15v2.813a6.471 6.471 0 0 1 1.5.709V6.25ZM6.25 4.5h11.5c.966 0 1.75.784 1.75 1.75V7h-15v-.75c0-.966.784-1.75 1.75-1.75Z", + "M23 17.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0Zm-5.5 0h2a.5.5 0 0 1 0 1H17a.5.5 0 0 1-.5-.491v-3.01a.5.5 0 0 1 1 0V17.5Z" + ], "call-outline": "m7.056 2.418 1.167-.351a2.75 2.75 0 0 1 3.302 1.505l.902 2.006a2.75 2.75 0 0 1-.633 3.139L10.3 10.11a.25.25 0 0 0-.078.155c-.044.397.225 1.17.845 2.245.451.781.86 1.33 1.207 1.637.242.215.375.261.432.245l2.01-.615a2.75 2.75 0 0 1 3.034 1.02l1.281 1.776a2.75 2.75 0 0 1-.339 3.605l-.886.84a3.75 3.75 0 0 1-3.587.889c-2.754-.769-5.223-3.093-7.435-6.924-2.215-3.836-2.992-7.14-2.276-9.913a3.75 3.75 0 0 1 2.548-2.652Zm.433 1.437a2.25 2.25 0 0 0-1.529 1.59c-.602 2.332.087 5.261 2.123 8.788 2.033 3.522 4.222 5.582 6.54 6.23a2.25 2.25 0 0 0 2.151-.534l.887-.84a1.25 1.25 0 0 0 .154-1.639l-1.28-1.775a1.25 1.25 0 0 0-1.38-.464l-2.015.617c-1.17.348-2.232-.593-3.372-2.568C9 11.93 8.642 10.9 8.731 10.099c.047-.416.24-.8.546-1.086l1.494-1.393a1.25 1.25 0 0 0 .288-1.427l-.902-2.006a1.25 1.25 0 0 0-1.5-.684l-1.168.352Z", "chat-help-outline": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10a9.96 9.96 0 0 1-4.587-1.112l-3.826 1.067a1.25 1.25 0 0 1-1.54-1.54l1.068-3.823A9.96 9.96 0 0 1 2 12C2 6.477 6.477 2 12 2Zm0 1.5A8.5 8.5 0 0 0 3.5 12c0 1.47.373 2.883 1.073 4.137l.15.27-1.112 3.984 3.987-1.112.27.15A8.5 8.5 0 1 0 12 3.5Zm0 12a1 1 0 1 1 0 2 1 1 0 0 1 0-2Zm0-8.75a2.75 2.75 0 0 1 2.75 2.75c0 1.01-.297 1.574-1.051 2.359l-.169.171c-.622.622-.78.886-.78 1.47a.75.75 0 0 1-1.5 0c0-1.01.297-1.574 1.051-2.359l.169-.171c.622-.622.78-.886.78-1.47a1.25 1.25 0 0 0-2.493-.128l-.007.128a.75.75 0 0 1-1.5 0A2.75 2.75 0 0 1 12 6.75Z", "chat-multiple-outline": "M9.562 3a7.5 7.5 0 0 0-6.798 10.673l-.724 2.842a1.25 1.25 0 0 0 1.504 1.524c.75-.18 1.903-.457 2.93-.702A7.5 7.5 0 1 0 9.561 3Zm-6 7.5a6 6 0 1 1 3.33 5.375l-.244-.121-.264.063c-.923.22-1.99.475-2.788.667l.69-2.708.07-.276-.13-.253a5.971 5.971 0 0 1-.664-2.747Zm11 10.5c-1.97 0-3.762-.759-5.1-2h.1c.718 0 1.415-.089 2.08-.257.865.482 1.86.757 2.92.757.96 0 1.866-.225 2.67-.625l.243-.121.264.063c.922.22 1.966.445 2.74.61-.175-.751-.414-1.756-.642-2.651l-.07-.276.13-.253a5.971 5.971 0 0 0 .665-2.747 5.995 5.995 0 0 0-2.747-5.042 8.44 8.44 0 0 0-.8-2.047 7.503 7.503 0 0 1 4.344 10.263c.253 1.008.509 2.1.671 2.803a1.244 1.244 0 0 1-1.467 1.5 132.62 132.62 0 0 1-2.913-.64 7.476 7.476 0 0 1-3.088.663Z", "chat-outline": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10a9.96 9.96 0 0 1-4.587-1.112l-3.826 1.067a1.25 1.25 0 0 1-1.54-1.54l1.068-3.823A9.96 9.96 0 0 1 2 12C2 6.477 6.477 2 12 2Zm0 1.5A8.5 8.5 0 0 0 3.5 12c0 1.47.373 2.883 1.073 4.137l.15.27-1.112 3.984 3.987-1.112.27.15A8.5 8.5 0 1 0 12 3.5ZM8.75 13h4.498a.75.75 0 0 1 .102 1.493l-.102.007H8.75a.75.75 0 0 1-.102-1.493L8.75 13h4.498H8.75Zm0-3.5h6.505a.75.75 0 0 1 .101 1.493l-.101.007H8.75a.75.75 0 0 1-.102-1.493L8.75 9.5h6.505H8.75Z", "checkmark-circle-outline": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm0 1.5a8.5 8.5 0 1 0 0 17 8.5 8.5 0 0 0 0-17Zm-1.25 9.94 4.47-4.47a.75.75 0 0 1 1.133.976l-.073.084-5 5a.75.75 0 0 1-.976.073l-.084-.073-2.5-2.5a.75.75 0 0 1 .976-1.133l.084.073 1.97 1.97 4.47-4.47-4.47 4.47Z", "checkmark-circle-solid": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm3.22 6.97-4.47 4.47-1.97-1.97a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l5-5a.75.75 0 1 0-1.06-1.06Z", + "checkmark-square-solid": "M18 3a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h12zm-1.53 4.97L10 14.44l-2.47-2.47a.75.75 0 0 0-1.06 1.06l3 3a.75.75 0 0 0 1.06 0l7-7a.75.75 0 0 0-1.06-1.06z", "checkmark-outline": "M4.53 12.97a.75.75 0 0 0-1.06 1.06l4.5 4.5a.75.75 0 0 0 1.06 0l11-11a.75.75 0 0 0-1.06-1.06L8.5 16.94l-3.97-3.97Z", "chevron-down-outline": "M4.22 8.47a.75.75 0 0 1 1.06 0L12 15.19l6.72-6.72a.75.75 0 1 1 1.06 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L4.22 9.53a.75.75 0 0 1 0-1.06Z", "chevron-left-outline": "M15.53 4.22a.75.75 0 0 1 0 1.06L8.81 12l6.72 6.72a.75.75 0 1 1-1.06 1.06l-7.25-7.25a.75.75 0 0 1 0-1.06l7.25-7.25a.75.75 0 0 1 1.06 0Z", @@ -34,6 +46,17 @@ "cloud-outline": "M6.087 9.75a5.752 5.752 0 0 1 11.326 0h.087a4 4 0 0 1 0 8H6a4 4 0 0 1 0-8h.087ZM11.75 6.5a4.25 4.25 0 0 0-4.245 4.037.75.75 0 0 1-.749.713H6a2.5 2.5 0 0 0 0 5h11.5a2.5 2.5 0 0 0 0-5h-.756a.75.75 0 0 1-.75-.713A4.25 4.25 0 0 0 11.75 6.5Z", "code-outline": "m8.066 18.943 6.5-14.5a.75.75 0 0 1 1.404.518l-.036.096-6.5 14.5a.75.75 0 0 1-1.404-.518l.036-.096 6.5-14.5-6.5 14.5ZM2.22 11.47l4.25-4.25a.75.75 0 0 1 1.133.976l-.073.085L3.81 12l3.72 3.719a.75.75 0 0 1-.976 1.133l-.084-.073-4.25-4.25a.75.75 0 0 1-.073-.976l.073-.084 4.25-4.25-4.25 4.25Zm14.25-4.25a.75.75 0 0 1 .976-.073l.084.073 4.25 4.25a.75.75 0 0 1 .073.976l-.073.085-4.25 4.25a.75.75 0 0 1-1.133-.977l.073-.084L20.19 12l-3.72-3.72a.75.75 0 0 1 0-1.06Z", "contact-card-group-outline": "M18.75 4A3.25 3.25 0 0 1 22 7.25v9.505a3.25 3.25 0 0 1-3.25 3.25H5.25A3.25 3.25 0 0 1 2 16.755V7.25a3.25 3.25 0 0 1 3.066-3.245L5.25 4h13.5Zm0 1.5H5.25l-.144.006A1.75 1.75 0 0 0 3.5 7.25v9.505c0 .966.784 1.75 1.75 1.75h13.5a1.75 1.75 0 0 0 1.75-1.75V7.25a1.75 1.75 0 0 0-1.75-1.75Zm-9.497 7a.75.75 0 0 1 .75.75v.582c0 1.272-.969 1.918-2.502 1.918S5 15.104 5 13.831v-.581a.75.75 0 0 1 .75-.75h3.503Zm1.58-.001 1.417.001a.75.75 0 0 1 .75.75v.333c0 .963-.765 1.417-1.875 1.417-.116 0-.229-.005-.337-.015a2.85 2.85 0 0 0 .206-.9l.009-.253v-.582c0-.269-.061-.524-.17-.751Zm4.417.001h3a.75.75 0 0 1 .102 1.493L18.25 14h-3a.75.75 0 0 1-.102-1.493l.102-.007h3-3Zm-7.75-4a1.5 1.5 0 1 1 0 3.001 1.5 1.5 0 0 1 0-3.001Zm3.87.502a1.248 1.248 0 1 1 0 2.496 1.248 1.248 0 0 1 0-2.496Zm3.88.498h3a.75.75 0 0 1 .102 1.493L18.25 11h-3a.75.75 0 0 1-.102-1.493l.102-.007h3-3Z", + "copy-outline": [ + "M8 3a1 1 0 0 0-1 1v.5a.5.5 0 0 1-1 0V4a2 2 0 0 1 2-2h.5a.5.5 0 0 1 0 1H8z", + "M7 12a1 1 0 0 0 1 1h.5a.5.5 0 0 1 0 1H8a2 2 0 0 1-2-2v-.5a.5.5 0 0 1 1 0v.5z", + "M7 6.5a.5.5 0 0 0-1 0v3a.5.5 0 0 0 1 0v-3z", + "M16 3a1 1 0 0 1 1 1v.5a.5.5 0 0 0 1 0V4a2 2 0 0 0-2-2h-.5a.5.5 0 0 0 0 1h.5z", + "M16 13a1 1 0 0 0 1-1v-.5a.5.5 0 0 1 1 0v.5a2 2 0 0 1-2 2h-.5a.5.5 0 0 1 0-1h.5z", + "M17.5 6a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 1 0v-3a.5.5 0 0 0-.5-.5z", + "M10.5 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1h-3z", + "M10 13.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5z", + "M4 6h1v6.5A2.5 2.5 0 0 0 7.5 15H14v1a2 2 0 0 1-2 2H5.5A3.5 3.5 0 0 1 2 14.5V8a2 2 0 0 1 2-2z" + ], "delete-outline": "M12 1.75a3.25 3.25 0 0 1 3.245 3.066L15.25 5h5.25a.75.75 0 0 1 .102 1.493L20.5 6.5h-.796l-1.28 13.02a2.75 2.75 0 0 1-2.561 2.474l-.176.006H8.313a2.75 2.75 0 0 1-2.714-2.307l-.023-.174L4.295 6.5H3.5a.75.75 0 0 1-.743-.648L2.75 5.75a.75.75 0 0 1 .648-.743L3.5 5h5.25A3.25 3.25 0 0 1 12 1.75Zm6.197 4.75H5.802l1.267 12.872a1.25 1.25 0 0 0 1.117 1.122l.127.006h7.374c.6 0 1.109-.425 1.225-1.002l.02-.126L18.196 6.5ZM13.75 9.25a.75.75 0 0 1 .743.648L14.5 10v7a.75.75 0 0 1-1.493.102L13 17v-7a.75.75 0 0 1 .75-.75Zm-3.5 0a.75.75 0 0 1 .743.648L11 10v7a.75.75 0 0 1-1.493.102L9.5 17v-7a.75.75 0 0 1 .75-.75Zm1.75-6a1.75 1.75 0 0 0-1.744 1.606L10.25 5h3.5A1.75 1.75 0 0 0 12 3.25Z", "dismiss-circle-outline": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm0 1.5a8.5 8.5 0 1 0 0 17 8.5 8.5 0 0 0 0-17Zm3.446 4.897.084.073a.75.75 0 0 1 .073.976l-.073.084L13.061 12l2.47 2.47a.75.75 0 0 1 .072.976l-.073.084a.75.75 0 0 1-.976.073l-.084-.073L12 13.061l-2.47 2.47a.75.75 0 0 1-.976.072l-.084-.073a.75.75 0 0 1-.073-.976l.073-.084L10.939 12l-2.47-2.47a.75.75 0 0 1-.072-.976l.073-.084a.75.75 0 0 1 .976-.073l.084.073L12 10.939l2.47-2.47a.75.75 0 0 1 .976-.072Z", "dismiss-outline": "m4.397 4.554.073-.084a.75.75 0 0 1 .976-.073l.084.073L12 10.939l6.47-6.47a.75.75 0 1 1 1.06 1.061L13.061 12l6.47 6.47a.75.75 0 0 1 .072.976l-.073.084a.75.75 0 0 1-.976.073l-.084-.073L12 13.061l-6.47 6.47a.75.75 0 0 1-1.06-1.061L10.939 12l-6.47-6.47a.75.75 0 0 1-.072-.976l.073-.084-.073.084Z", @@ -80,6 +103,7 @@ "sound-source-outline": "M3.5 12a8.5 8.5 0 1 1 14.762 5.748l.992 1.135A9.966 9.966 0 0 0 22 12c0-5.523-4.477-10-10-10S2 6.477 2 12a9.966 9.966 0 0 0 2.746 6.883l.993-1.134A8.47 8.47 0 0 1 3.5 12Z M19.25 12.125a7.098 7.098 0 0 1-1.783 4.715l-.998-1.14a5.625 5.625 0 1 0-8.806-.15l-1.004 1.146a7.125 7.125 0 1 1 12.59-4.571Z M16.25 12a4.23 4.23 0 0 1-.821 2.511l-1.026-1.172a2.75 2.75 0 1 0-4.806 0L8.571 14.51A4.25 4.25 0 1 1 16.25 12Z M12.564 12.756a.75.75 0 0 0-1.128 0l-7 8A.75.75 0 0 0 5 22h14a.75.75 0 0 0 .564-1.244l-7-8Zm4.783 7.744H6.653L12 14.389l5.347 6.111Z", "speaker-1-outline": "M14.704 3.442c.191.226.296.512.296.808v15.502a1.25 1.25 0 0 1-2.058.954L7.975 16.5H4.25A2.25 2.25 0 0 1 2 14.25v-4.5A2.25 2.25 0 0 1 4.25 7.5h3.725l4.968-4.204a1.25 1.25 0 0 1 1.761.147ZM13.5 4.79 8.525 9H4.25a.75.75 0 0 0-.75.75v4.5c0 .415.336.75.75.75h4.275l4.975 4.213V4.79Zm3.604 3.851a.75.75 0 0 1 1.03.25c.574.94.862 1.992.862 3.14 0 1.149-.288 2.201-.862 3.141a.75.75 0 1 1-1.28-.781c.428-.702.642-1.483.642-2.36 0-.876-.214-1.657-.642-2.359a.75.75 0 0 1 .25-1.03Z", "speaker-mute-outline": "M12.92 3.316c.806-.717 2.08-.145 2.08.934v15.496c0 1.078-1.274 1.65-2.08.934l-4.492-3.994a.75.75 0 0 0-.498-.19H4.25A2.25 2.25 0 0 1 2 14.247V9.75a2.25 2.25 0 0 1 2.25-2.25h3.68a.75.75 0 0 0 .498-.19l4.491-3.993Zm.58 1.49L9.425 8.43A2.25 2.25 0 0 1 7.93 9H4.25a.75.75 0 0 0-.75.75v4.497c0 .415.336.75.75.75h3.68a2.25 2.25 0 0 1 1.495.57l4.075 3.623V4.807ZM16.22 9.22a.75.75 0 0 1 1.06 0L19 10.94l1.72-1.72a.75.75 0 1 1 1.06 1.06L20.06 12l1.72 1.72a.75.75 0 1 1-1.06 1.06L19 13.06l-1.72 1.72a.75.75 0 1 1-1.06-1.06L17.94 12l-1.72-1.72a.75.75 0 0 1 0-1.06Z", + "square-outline": "M3 6a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6zm3-2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H6z", "star-emphasis-outline": "M13.209 3.103c-.495-1.004-1.926-1.004-2.421 0L8.43 7.88l-5.273.766c-1.107.161-1.55 1.522-.748 2.303l3.815 3.72-.9 5.25c-.19 1.103.968 1.944 1.959 1.424l4.715-2.48 4.716 2.48c.99.52 2.148-.32 1.96-1.424l-.902-5.25 3.816-3.72c.8-.78.359-2.142-.748-2.303l-5.273-.766-2.358-4.777ZM9.74 8.615l2.258-4.576 2.259 4.576a1.35 1.35 0 0 0 1.016.738l5.05.734-3.654 3.562a1.35 1.35 0 0 0-.388 1.195l.862 5.03-4.516-2.375a1.35 1.35 0 0 0-1.257 0l-4.516 2.374.862-5.029a1.35 1.35 0 0 0-.388-1.195l-3.654-3.562 5.05-.734c.44-.063.82-.34 1.016-.738ZM1.164 3.782a.75.75 0 0 0 .118 1.054l2.5 2a.75.75 0 1 0 .937-1.172l-2.5-2a.75.75 0 0 0-1.055.118Z M22.836 18.218a.75.75 0 0 0-.117-1.054l-2.5-2a.75.75 0 0 0-.938 1.172l2.5 2a.75.75 0 0 0 1.055-.117ZM1.282 17.164a.75.75 0 1 0 .937 1.172l2.5-2a.75.75 0 0 0-.937-1.172l-2.5 2ZM22.836 3.782a.75.75 0 0 1-.117 1.054l-2.5 2a.75.75 0 0 1-.938-1.172l2.5-2a.75.75 0 0 1 1.055.118Z", "subtract-outline": "M3.997 13H20a1 1 0 1 0 0-2H3.997a1 1 0 1 0 0 2Z", "tag-outline": "M19.75 2A2.25 2.25 0 0 1 22 4.25v5.462a3.25 3.25 0 0 1-.952 2.298l-8.5 8.503a3.255 3.255 0 0 1-4.597.001L3.489 16.06a3.25 3.25 0 0 1-.003-4.596l8.5-8.51A3.25 3.25 0 0 1 14.284 2h5.465Zm0 1.5h-5.465c-.465 0-.91.185-1.239.513l-8.512 8.523a1.75 1.75 0 0 0 .015 2.462l4.461 4.454a1.755 1.755 0 0 0 2.477 0l8.5-8.503a1.75 1.75 0 0 0 .513-1.237V4.25a.75.75 0 0 0-.75-.75ZM17 5.502a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Z", @@ -87,7 +111,6 @@ "video-outline": "M13.75 4.5A3.25 3.25 0 0 1 17 7.75v.173l3.864-2.318A.75.75 0 0 1 22 6.248V17.75a.75.75 0 0 1-1.136.643L17 16.075v.175a3.25 3.25 0 0 1-3.25 3.25h-8.5A3.25 3.25 0 0 1 2 16.25v-8.5A3.25 3.25 0 0 1 5.25 4.5h8.5Zm0 1.5h-8.5A1.75 1.75 0 0 0 3.5 7.75v8.5c0 .966.784 1.75 1.75 1.75h8.5a1.75 1.75 0 0 0 1.75-1.75v-8.5A1.75 1.75 0 0 0 13.75 6Zm6.75 1.573L17 9.674v4.651l3.5 2.1V7.573Z", "warning-outline": "M10.91 2.782a2.25 2.25 0 0 1 2.975.74l.083.138 7.759 14.009a2.25 2.25 0 0 1-1.814 3.334l-.154.006H4.243a2.25 2.25 0 0 1-2.041-3.197l.072-.143L10.031 3.66a2.25 2.25 0 0 1 .878-.878Zm9.505 15.613-7.76-14.008a.75.75 0 0 0-1.254-.088l-.057.088-7.757 14.008a.75.75 0 0 0 .561 1.108l.095.006h15.516a.75.75 0 0 0 .696-1.028l-.04-.086-7.76-14.008 7.76 14.008ZM12 16.002a.999.999 0 1 1 0 1.997.999.999 0 0 1 0-1.997ZM11.995 8.5a.75.75 0 0 1 .744.647l.007.102.004 4.502a.75.75 0 0 1-1.494.103l-.006-.102-.004-4.502a.75.75 0 0 1 .75-.75Z", "wifi-off-outline": "m12.858 14.273 7.434 7.434a1 1 0 0 0 1.414-1.414l-17.999-18a1 1 0 1 0-1.414 1.414L5.39 6.804c-.643.429-1.254.927-1.821 1.495a12.382 12.382 0 0 0-1.39 1.683 1 1 0 0 0 1.644 1.14c.363-.524.761-1.01 1.16-1.41a9.94 9.94 0 0 1 1.855-1.46L7.99 9.405a8.14 8.14 0 0 0-3.203 3.377 1 1 0 0 0 1.784.903 6.08 6.08 0 0 1 1.133-1.563 6.116 6.116 0 0 1 1.77-1.234l1.407 1.407A5.208 5.208 0 0 0 8.336 13.7a5.25 5.25 0 0 0-1.09 1.612 1 1 0 0 0 1.832.802c.167-.381.394-.722.672-1a3.23 3.23 0 0 1 3.108-.841Zm-1.332-5.93 2.228 2.229a6.1 6.1 0 0 1 2.616 1.55c.444.444.837.995 1.137 1.582a1 1 0 1 0 1.78-.911 8.353 8.353 0 0 0-1.503-2.085 8.108 8.108 0 0 0-6.258-2.365ZM8.51 5.327l1.651 1.651a9.904 9.904 0 0 1 10.016 4.148 1 1 0 1 0 1.646-1.136A11.912 11.912 0 0 0 8.51 5.327Zm4.552 11.114a1.501 1.501 0 1 1-2.123 2.123 1.501 1.501 0 0 1 2.123-2.123Z", - "brand-facebook-outline": "M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z", "brand-line-outline": "M19.365 9.863c.349 0 .63.285.63.631 0 .345-.281.63-.63.63H17.61v1.125h1.755c.349 0 .63.283.63.63 0 .344-.281.629-.63.629h-2.386c-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63h2.386c.346 0 .627.285.627.63 0 .349-.281.63-.63.63H17.61v1.125h1.755zm-3.855 3.016c0 .27-.174.51-.432.596-.064.021-.133.031-.199.031-.211 0-.391-.09-.51-.25l-2.443-3.317v2.94c0 .344-.279.629-.631.629-.346 0-.626-.285-.626-.629V8.108c0-.27.173-.51.43-.595.06-.023.136-.033.194-.033.195 0 .375.104.495.254l2.462 3.33V8.108c0-.345.282-.63.63-.63.345 0 .63.285.63.63v4.771zm-5.741 0c0 .344-.282.629-.631.629-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63.346 0 .628.285.628.63v4.771zm-2.466.629H4.917c-.345 0-.63-.285-.63-.629V8.108c0-.345.285-.63.63-.63.348 0 .63.285.63.63v4.141h1.756c.348 0 .629.283.629.63 0 .344-.282.629-.629.629M24 10.314C24 4.943 18.615.572 12 .572S0 4.943 0 10.314c0 4.811 4.27 8.842 10.035 9.608.391.082.923.258 1.058.59.12.301.079.766.038 1.08l-.164 1.02c-.045.301-.24 1.186 1.049.645 1.291-.539 6.916-4.078 9.436-6.975C23.176 14.393 24 12.458 24 10.314", "brand-linkedin-outline": "M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z", @@ -95,7 +118,6 @@ "brand-telegram-outline": "M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z", "brand-twitter-outline": "M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z", "brand-whatsapp-outline": "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z", - "add-solid": "M11.883 3.007 12 3a1 1 0 0 1 .993.883L13 4v7h7a1 1 0 0 1 .993.883L21 12a1 1 0 0 1-.883.993L20 13h-7v7a1 1 0 0 1-.883.993L12 21a1 1 0 0 1-.993-.883L11 20v-7H4a1 1 0 0 1-.993-.883L3 12a1 1 0 0 1 .883-.993L4 11h7V4a1 1 0 0 1 .883-.993L12 3l-.117.007Z", "subtract-solid": "M3.997 13H20a1 1 0 1 0 0-2H3.997a1 1 0 1 0 0 2Z" } diff --git a/app/listeners/automation_rule_listener.rb b/app/listeners/automation_rule_listener.rb index cfd97e560..5b4893763 100644 --- a/app/listeners/automation_rule_listener.rb +++ b/app/listeners/automation_rule_listener.rb @@ -14,7 +14,7 @@ class AutomationRuleListener < BaseListener return unless rule_present?('conversation_created', conversation) @rules.each do |rule| - conditions_match = ::AutomationRule::ConditionsFilterService.new(rule, conversation).perform + conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, conversation).perform ::AutomationRules::ActionService.new(rule, conversation).perform if conditions_match.present? end end From 193a531e4998b2cf0deafb7466109b094e000caf Mon Sep 17 00:00:00 2001 From: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com> Date: Wed, 2 Feb 2022 19:15:02 +0530 Subject: [PATCH 048/101] feat: Edit automation modal (#3876) * Add automation modal * Fix the v-model for automations * Actions and Condition dropdowns for automations * Fix merge conflicts * Handle event change and confirmation * Appends new action * Removes actions * Automation api integration * Api integration for creating automations * Registers vuex module to the global store * Automations table * Updarted labels and actions * Integrate automation api * Fixes the mutation error - removed the data key wrapper * Fixed the automation condition models to work with respective event types * Remove temporary fixes added to the api request * Displa timestamp and automation status values * Added the clone buton * Removed uncessary helper method * Specs for automations * Handle WIP code * Remove the payload wrap * Fix the action query payload * Removed unnecessary files * Disabled Automations routes * Ability to delete automations * Fix specs * Edit automation modal * Edit automation modal and api integration * Replaced hardcoded values * Using absolute paths * Update app/javascript/dashboard/routes/dashboard/settings/automation/EditAutomationRule.vue Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> * Update app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> * Intendation fix * Disable automation route * Minor fix Co-authored-by: Muhsin Keloth Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> --- .../layout/config/sidebarItems/settings.js | 14 +- .../settings/automation/AddAutomationRule.vue | 6 +- .../automation/EditAutomationRule.vue | 479 ++++++++++++++++++ .../dashboard/settings/automation/Index.vue | 43 +- .../dashboard/store/modules/automations.js | 3 +- 5 files changed, 526 insertions(+), 19 deletions(-) create mode 100644 app/javascript/dashboard/routes/dashboard/settings/automation/EditAutomationRule.vue diff --git a/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js b/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js index 176fd152c..577d41c6b 100644 --- a/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js +++ b/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js @@ -69,13 +69,13 @@ const settings = accountId => ({ ), toStateName: 'attributes_list', }, - { - icon: 'automation', - label: 'AUTOMATION', - hasSubMenu: false, - toState: frontendURL(`accounts/${accountId}/settings/automation/list`), - toStateName: 'automation_list', - }, + // { + // icon: 'automation', + // label: 'AUTOMATION', + // hasSubMenu: false, + // toState: frontendURL(`accounts/${accountId}/settings/automation/list`), + // toStateName: 'automation_list', + // }, { icon: 'chat-multiple', label: 'CANNED_RESPONSES', diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/AddAutomationRule.vue b/app/javascript/dashboard/routes/dashboard/settings/automation/AddAutomationRule.vue index d26a5695b..3f9d2d83e 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/automation/AddAutomationRule.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/automation/AddAutomationRule.vue @@ -434,13 +434,13 @@ export default { } .event_wrapper { select { - margin: 0; + margin: var(--space-zero); } .info-message { font-size: var(--font-size-mini); - color: #868686; + color: var(--s-500); text-align: right; } - margin-bottom: 1.6rem; + margin-bottom: var(--space-medium); } diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/EditAutomationRule.vue b/app/javascript/dashboard/routes/dashboard/settings/automation/EditAutomationRule.vue new file mode 100644 index 000000000..223c9577f --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/automation/EditAutomationRule.vue @@ -0,0 +1,479 @@ + + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue index 39c83c5fe..6c1892a28 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue @@ -43,7 +43,7 @@ {{ readableTime(automation.created_on) }} - + { + commit(types.SET_AUTOMATION_UI_FLAG, { isCloning: true }); + try { + await AutomationAPI.clone(id); + } catch (error) { + throw new Error(error); + } finally { + commit(types.SET_AUTOMATION_UI_FLAG, { isCloning: false }); + } + }, }; export const mutations = { diff --git a/app/javascript/dashboard/store/modules/specs/automations/actions.spec.js b/app/javascript/dashboard/store/modules/specs/automations/actions.spec.js index 8dbc08f00..9ac38a5bc 100644 --- a/app/javascript/dashboard/store/modules/specs/automations/actions.spec.js +++ b/app/javascript/dashboard/store/modules/specs/automations/actions.spec.js @@ -47,28 +47,28 @@ describe('#actions', () => { ]); }); }); - // API Work in progress - // describe('#update', () => { - // it('sends correct actions if API is success', async () => { - // axios.patch.mockResolvedValue({ data: automationsList[0] }); - // await actions.update({ commit }, automationsList[0]); - // expect(commit.mock.calls).toEqual([ - // [types.default.SET_AUTOMATION_UI_FLAG, { isUpdating: true }], - // [types.default.EDIT_AUTOMATION, automationsList[0]], - // [types.default.SET_AUTOMATION_UI_FLAG, { isUpdating: false }], - // ]); - // }); - // it('sends correct actions if API is error', async () => { - // axios.patch.mockRejectedValue({ message: 'Incorrect header' }); - // await expect( - // actions.update({ commit }, automationsList[0]) - // ).rejects.toThrow(Error); - // expect(commit.mock.calls).toEqual([ - // [types.default.SET_AUTOMATION_UI_FLAG, { isUpdating: true }], - // [types.default.SET_AUTOMATION_UI_FLAG, { isUpdating: false }], - // ]); - // }); - // }); + + describe('#update', () => { + it('sends correct actions if API is success', async () => { + axios.patch.mockResolvedValue({ data: automationsList[0] }); + await actions.update({ commit }, automationsList[0]); + expect(commit.mock.calls).toEqual([ + [types.default.SET_AUTOMATION_UI_FLAG, { isUpdating: true }], + [types.default.EDIT_AUTOMATION, automationsList[0]], + [types.default.SET_AUTOMATION_UI_FLAG, { isUpdating: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.patch.mockRejectedValue({ message: 'Incorrect header' }); + await expect( + actions.update({ commit }, automationsList[0]) + ).rejects.toThrow(Error); + expect(commit.mock.calls).toEqual([ + [types.default.SET_AUTOMATION_UI_FLAG, { isUpdating: true }], + [types.default.SET_AUTOMATION_UI_FLAG, { isUpdating: false }], + ]); + }); + }); describe('#delete', () => { it('sends correct actions if API is success', async () => { @@ -91,4 +91,15 @@ describe('#actions', () => { ]); }); }); + + describe('#clone', () => { + it('clones the automation', async () => { + axios.post.mockResolvedValue({ data: automationsList[0] }); + await actions.clone({ commit }, automationsList[0]); + expect(commit.mock.calls).toEqual([ + [types.default.SET_AUTOMATION_UI_FLAG, { isCloning: true }], + [types.default.SET_AUTOMATION_UI_FLAG, { isCloning: false }], + ]); + }); + }); }); From 903072ef14807b3ae96596951452d30b412bd237 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Thu, 3 Feb 2022 10:05:56 +0530 Subject: [PATCH 052/101] chore: Add custom attributes in conversation webhook payload (#3839) --- app/presenters/conversations/event_data_presenter.rb | 1 + spec/models/conversation_spec.rb | 1 + spec/presenters/conversations/event_data_presenter_spec.rb | 1 + 3 files changed, 3 insertions(+) diff --git a/app/presenters/conversations/event_data_presenter.rb b/app/presenters/conversations/event_data_presenter.rb index b4a443f35..9f3b1dced 100644 --- a/app/presenters/conversations/event_data_presenter.rb +++ b/app/presenters/conversations/event_data_presenter.rb @@ -10,6 +10,7 @@ class Conversations::EventDataPresenter < SimpleDelegator messages: push_messages, meta: push_meta, status: status, + custom_attributes: custom_attributes, snoozed_until: snoozed_until, unread_count: unread_incoming_messages.count, **push_timestamps diff --git a/spec/models/conversation_spec.rb b/spec/models/conversation_spec.rb index 67a6c7c83..555957e77 100644 --- a/spec/models/conversation_spec.rb +++ b/spec/models/conversation_spec.rb @@ -386,6 +386,7 @@ RSpec.describe Conversation, type: :model do can_reply: true, channel: 'Channel::WebWidget', snoozed_until: conversation.snoozed_until, + custom_attributes: conversation.custom_attributes, contact_last_seen_at: conversation.contact_last_seen_at.to_i, agent_last_seen_at: conversation.agent_last_seen_at.to_i, unread_count: 0 diff --git a/spec/presenters/conversations/event_data_presenter_spec.rb b/spec/presenters/conversations/event_data_presenter_spec.rb index 88cd89f0a..b22057435 100644 --- a/spec/presenters/conversations/event_data_presenter_spec.rb +++ b/spec/presenters/conversations/event_data_presenter_spec.rb @@ -24,6 +24,7 @@ RSpec.describe Conversations::EventDataPresenter do channel: conversation.inbox.channel_type, timestamp: conversation.last_activity_at.to_i, snoozed_until: conversation.snoozed_until, + custom_attributes: conversation.custom_attributes, contact_last_seen_at: conversation.contact_last_seen_at.to_i, agent_last_seen_at: conversation.agent_last_seen_at.to_i, unread_count: 0 From dbb50e59233d1bf7c78093e78432b7b42f181b0d Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Thu, 3 Feb 2022 10:11:02 +0530 Subject: [PATCH 053/101] fix: Update font-size/width to fix iOS input zoom (#3894) --- app/javascript/widget/assets/scss/_forms.scss | 4 ++-- app/javascript/widget/components/ChatInputWrap.vue | 3 ++- app/javascript/widget/components/template/EmailInput.vue | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/javascript/widget/assets/scss/_forms.scss b/app/javascript/widget/assets/scss/_forms.scss index 6ea4666aa..05d3fd26d 100755 --- a/app/javascript/widget/assets/scss/_forms.scss +++ b/app/javascript/widget/assets/scss/_forms.scss @@ -15,9 +15,9 @@ $input-height: $space-two * 2; color: $color-body; display: block; font-family: $font-family; - font-size: $font-size-default; + font-size: $font-size-medium; height: $input-height; - line-height: 1.3; + line-height: 1.5; max-width: 100%; outline: none; padding: $space-smaller; diff --git a/app/javascript/widget/components/ChatInputWrap.vue b/app/javascript/widget/components/ChatInputWrap.vue index e9f00773e..741eb67b2 100755 --- a/app/javascript/widget/components/ChatInputWrap.vue +++ b/app/javascript/widget/components/ChatInputWrap.vue @@ -196,7 +196,8 @@ export default { max-height: 2.4 * $space-mega; resize: none; padding: 0; - padding-top: $space-small; + padding-top: $space-smaller; + padding-bottom: $space-smaller; margin-top: $space-small; margin-bottom: $space-small; } diff --git a/app/javascript/widget/components/template/EmailInput.vue b/app/javascript/widget/components/template/EmailInput.vue index d133ccda3..096870b73 100644 --- a/app/javascript/widget/components/template/EmailInput.vue +++ b/app/javascript/widget/components/template/EmailInput.vue @@ -103,7 +103,7 @@ export default { border-bottom-right-radius: 0; border-top-right-radius: 0; padding: $space-one; - width: auto; + width: 100%; &.error { border-color: $color-error; From 8dcb4a5ed418128be4dd8aa07d38d2091798a8f9 Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Thu, 3 Feb 2022 12:05:39 +0530 Subject: [PATCH 054/101] chore: Add resolve action in Dialogflow Integration (#3900) --- lib/integrations/dialogflow/processor_service.rb | 2 ++ .../dialogflow/processor_service_spec.rb | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/lib/integrations/dialogflow/processor_service.rb b/lib/integrations/dialogflow/processor_service.rb index bb5485296..e9dfa8fb2 100644 --- a/lib/integrations/dialogflow/processor_service.rb +++ b/lib/integrations/dialogflow/processor_service.rb @@ -77,6 +77,8 @@ class Integrations::Dialogflow::ProcessorService case action when 'handoff' message.conversation.open! + when 'resolve' + message.conversation.resolved! end end end diff --git a/spec/lib/integrations/dialogflow/processor_service_spec.rb b/spec/lib/integrations/dialogflow/processor_service_spec.rb index 3971482d9..e002e1c3e 100644 --- a/spec/lib/integrations/dialogflow/processor_service_spec.rb +++ b/spec/lib/integrations/dialogflow/processor_service_spec.rb @@ -75,6 +75,20 @@ describe Integrations::Dialogflow::ProcessorService do end end + context 'when dialogflow returns resolve action' do + let(:dialogflow_response) do + ActiveSupport::HashWithIndifferentAccess.new( + fulfillment_messages: [{ payload: { action: 'resolve' } }, { text: dialogflow_text_double }] + ) + end + + it 'resolves the conversation without moving it to an agent' do + processor.perform + expect(conversation.reload.status).to eql('resolved') + expect(conversation.messages.last.content).to eql('hello payload') + end + end + context 'when conversation is not bot' do let(:conversation) { create(:conversation, account: account, status: :open) } From 5c6958482f55c5fe0ac1e0cb2674ba2cade517e7 Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Thu, 3 Feb 2022 12:14:34 +0530 Subject: [PATCH 055/101] fix: Shows tooltip and avatar for template messages (#3898) --- .../widgets/conversation/Message.vue | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/app/javascript/dashboard/components/widgets/conversation/Message.vue b/app/javascript/dashboard/components/widgets/conversation/Message.vue index 97fda5bcf..a71ba13d0 100644 --- a/app/javascript/dashboard/components/widgets/conversation/Message.vue +++ b/app/javascript/dashboard/components/widgets/conversation/Message.vue @@ -64,7 +64,7 @@ > Date: Thu, 3 Feb 2022 14:39:47 +0530 Subject: [PATCH 056/101] Fix: Custom filter distinct select (#3895) --- app/services/contacts/filter_service.rb | 2 +- spec/services/contacts/filter_service_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/services/contacts/filter_service.rb b/app/services/contacts/filter_service.rb index cc90b8502..aaa7412d4 100644 --- a/app/services/contacts/filter_service.rb +++ b/app/services/contacts/filter_service.rb @@ -18,7 +18,7 @@ class Contacts::FilterService < FilterService @query_string += contact_query_string(current_filter, query_hash, current_index) end - base_relation.select('distinct contacts.id').where(@query_string, @filter_values.with_indifferent_access) + base_relation.where(@query_string, @filter_values.with_indifferent_access) end def contact_query_string(current_filter, query_hash, current_index) diff --git a/spec/services/contacts/filter_service_spec.rb b/spec/services/contacts/filter_service_spec.rb index 14f4c1d27..f52a6c934 100644 --- a/spec/services/contacts/filter_service_spec.rb +++ b/spec/services/contacts/filter_service_spec.rb @@ -55,7 +55,7 @@ describe ::Contacts::FilterService do attribute_key: 'browser_language', filter_operator: 'equal_to', values: ['en'], - query_operator: 'AND' + query_operator: 'OR' }.with_indifferent_access, { attribute_key: 'name', @@ -69,7 +69,7 @@ describe ::Contacts::FilterService do it 'filter contacts by additional_attributes' do params[:payload] = payload result = filter_service.new(params, user_1).perform - expect(result[:contacts].length).to be 1 + expect(result[:count]).to be 2 end it 'filter contact by tags' do From cf10f3d03b666cd45b45df5d1fee28a533a21b46 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Thu, 3 Feb 2022 15:22:13 -0800 Subject: [PATCH 057/101] chore: Provider APIs for SMS Channel - Bandwidth (#3889) fixes: #3888 --- app/builders/contact_inbox_builder.rb | 20 +- .../api/v1/accounts/inboxes_controller.rb | 41 ++-- app/controllers/webhooks/sms_controller.rb | 6 + .../dashboard/i18n/locale/en/inboxMgmt.json | 52 ++++- .../settings/campaigns/AddCampaign.vue | 2 +- .../settings/campaigns/EditCampaign.vue | 2 +- .../dashboard/settings/inbox/ChannelList.vue | 2 +- .../dashboard/settings/inbox/FinishSetup.vue | 24 ++- .../routes/dashboard/settings/inbox/Index.vue | 3 + .../settings/inbox/channels/BandwidthSms.vue | 181 ++++++++++++++++++ .../dashboard/settings/inbox/channels/Sms.vue | 23 ++- .../dashboard/store/modules/inboxes.js | 6 +- .../store/modules/specs/inboxes/fixtures.js | 7 + .../modules/specs/inboxes/getters.spec.js | 6 +- app/javascript/shared/mixins/inboxMixin.js | 1 + app/jobs/send_reply_job.rb | 21 +- app/jobs/webhooks/sms_events_job.rb | 13 ++ app/models/account.rb | 1 + app/models/campaign.rb | 5 +- app/models/channel/sms.rb | 81 ++++++++ app/models/channel/whatsapp.rb | 2 +- app/models/inbox.rb | 2 + .../contacts/contactable_inboxes_service.rb | 8 + app/services/sms/incoming_message_service.rb | 66 +++++++ .../sms/oneoff_sms_campaign_service.rb | 32 ++++ app/services/sms/send_on_sms_service.rb | 16 ++ config/routes.rb | 1 + db/migrate/20220129024443_add_sms_channel.rb | 11 ++ db/schema.rb | 10 + spec/builders/contact_inbox_builder_spec.rb | 47 +++++ .../v1/accounts/inboxes_controller_spec.rb | 12 ++ .../webhooks/sms_controller_spec.rb | 12 ++ spec/factories/channel/channel_sms.rb | 16 ++ spec/jobs/send_reply_job_spec.rb | 9 + spec/jobs/webhooks/sms_events_job_spec.rb | 56 ++++++ spec/models/campaign_spec.rb | 21 ++ .../contactable_inboxes_service_spec.rb | 6 +- .../sms/incoming_message_service_spec.rb | 31 +++ .../sms/oneoff_sms_campaign_service_spec.rb | 47 +++++ spec/services/sms/send_on_sms_service_spec.rb | 28 +++ 40 files changed, 879 insertions(+), 51 deletions(-) create mode 100644 app/controllers/webhooks/sms_controller.rb create mode 100644 app/javascript/dashboard/routes/dashboard/settings/inbox/channels/BandwidthSms.vue create mode 100644 app/jobs/webhooks/sms_events_job.rb create mode 100644 app/models/channel/sms.rb create mode 100644 app/services/sms/incoming_message_service.rb create mode 100644 app/services/sms/oneoff_sms_campaign_service.rb create mode 100644 app/services/sms/send_on_sms_service.rb create mode 100644 db/migrate/20220129024443_add_sms_channel.rb create mode 100644 spec/controllers/webhooks/sms_controller_spec.rb create mode 100644 spec/factories/channel/channel_sms.rb create mode 100644 spec/jobs/webhooks/sms_events_job_spec.rb create mode 100644 spec/services/sms/incoming_message_service_spec.rb create mode 100644 spec/services/sms/oneoff_sms_campaign_service_spec.rb create mode 100644 spec/services/sms/send_on_sms_service_spec.rb diff --git a/app/builders/contact_inbox_builder.rb b/app/builders/contact_inbox_builder.rb index 8ee1b3fec..e7ae8b0aa 100644 --- a/app/builders/contact_inbox_builder.rb +++ b/app/builders/contact_inbox_builder.rb @@ -4,7 +4,7 @@ class ContactInboxBuilder def perform @contact = Contact.find(contact_id) @inbox = @contact.account.inboxes.find(inbox_id) - return unless ['Channel::TwilioSms', 'Channel::Email', 'Channel::Api', 'Channel::Whatsapp'].include? @inbox.channel_type + return unless ['Channel::TwilioSms', 'Channel::Sms', 'Channel::Email', 'Channel::Api', 'Channel::Whatsapp'].include? @inbox.channel_type source_id = @source_id || generate_source_id create_contact_inbox(source_id) if source_id.present? @@ -13,12 +13,18 @@ class ContactInboxBuilder private def generate_source_id - return twilio_source_id if @inbox.channel_type == 'Channel::TwilioSms' - return wa_source_id if @inbox.channel_type == 'Channel::Whatsapp' - return @contact.email if @inbox.channel_type == 'Channel::Email' - return SecureRandom.uuid if @inbox.channel_type == 'Channel::Api' - - nil + case @inbox.channel_type + when 'Channel::TwilioSms' + twilio_source_id + when 'Channel::Whatsapp' + wa_source_id + when 'Channel::Email' + @contact.email + when 'Channel::Sms' + @contact.phone_number + when 'Channel::Api' + SecureRandom.uuid + end end def wa_source_id diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 9f2cfba8c..62a471cda 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -91,20 +91,9 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController end def create_channel - case permitted_params[:channel][:type] - when 'web_widget' - Current.account.web_widgets.create!(permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel].except(:type)) - when 'api' - Current.account.api_channels.create!(permitted_params(Channel::Api::EDITABLE_ATTRS)[:channel].except(:type)) - when 'email' - Current.account.email_channels.create!(permitted_params(Channel::Email::EDITABLE_ATTRS)[:channel].except(:type)) - when 'line' - Current.account.line_channels.create!(permitted_params(Channel::Line::EDITABLE_ATTRS)[:channel].except(:type)) - when 'telegram' - Current.account.telegram_channels.create!(permitted_params(Channel::Telegram::EDITABLE_ATTRS)[:channel].except(:type)) - when 'whatsapp' - Current.account.whatsapp_channels.create!(permitted_params(Channel::Whatsapp::EDITABLE_ATTRS)[:channel].except(:type)) - end + return unless %w[web_widget api email line telegram whatsapp sms].include?(permitted_params[:channel][:type]) + + account_channels_method.create!(permitted_params(channel_type_from_params::EDITABLE_ATTRS)[:channel].except(:type)) end def update_channel_feature_flags @@ -123,6 +112,30 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController ) end + def channel_type_from_params + { + 'web_widget' => Channel::WebWidget, + 'api' => Channel::Api, + 'email' => Channel::Email, + 'line' => Channel::Line, + 'telegram' => Channel::Telegram, + 'whatsapp' => Channel::Whatsapp, + 'sms' => Channel::Sms + }[permitted_params[:channel][:type]] + end + + def account_channels_method + { + 'web_widget' => Current.account.web_widgets, + 'api' => Current.account.api_channels, + 'email' => Current.account.email_channels, + 'line' => Current.account.line_channels, + 'telegram' => Current.account.telegram_channels, + 'whatsapp' => Current.account.whatsapp_channels, + 'sms' => Current.account.sms_channels + }[permitted_params[:channel][:type]] + end + def get_channel_attributes(channel_type) if channel_type.constantize.const_defined?('EDITABLE_ATTRS') channel_type.constantize::EDITABLE_ATTRS.presence diff --git a/app/controllers/webhooks/sms_controller.rb b/app/controllers/webhooks/sms_controller.rb new file mode 100644 index 000000000..914357dc9 --- /dev/null +++ b/app/controllers/webhooks/sms_controller.rb @@ -0,0 +1,6 @@ +class Webhooks::SmsController < ActionController::API + def process_payload + Webhooks::SmsEventsJob.perform_later(params['_json']&.first&.to_unsafe_hash) + head :ok + end +end diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index 537998ac9..86cf1a3c3 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -136,8 +136,56 @@ } }, "SMS": { - "TITLE": "SMS Channel via Twilio", - "DESC": "Start supporting your customers via SMS with Twilio integration." + "TITLE": "SMS Channel", + "DESC": "Start supporting your customers via SMS.", + "PROVIDERS": { + "LABEL": "API Provider", + "TWILIO": "Twilio", + "BANDWIDTH": "Bandwidth" + }, + "API": { + "ERROR_MESSAGE": "We were not able to save the SMS channel" + }, + "BANDWIDTH": { + "ACCOUNT_ID": { + "LABEL": "Account ID", + "PLACEHOLDER": "Please enter your Bandwidth Account ID", + "ERROR": "This field is required" + }, + "API_KEY": { + "LABEL": "API Key", + "PLACEHOLDER": "Please enter your Bandwith API Key", + "ERROR": "This field is required" + }, + "API_SECRET": { + "LABEL": "API Secret", + "PLACEHOLDER": "Please enter your Bandwith API Secret", + "ERROR": "This field is required" + }, + "APPLICATION_ID": { + "LABEL": "Application ID", + "PLACEHOLDER": "Please enter your Bandwidth Application ID", + "ERROR": "This field is required" + }, + "INBOX_NAME": { + "LABEL": "Inbox Name", + "PLACEHOLDER": "Please enter a inbox name", + "ERROR": "This field is required" + }, + "PHONE_NUMBER": { + "LABEL": "Phone number", + "PLACEHOLDER": "Please enter the phone number from which message will be sent.", + "ERROR": "Please enter a valid value. Phone number should start with `+` sign." + }, + "SUBMIT_BUTTON": "Create Bandwidth Channel", + "API": { + "ERROR_MESSAGE": "We were not able to authenticate Bandwidth credentials, please try again" + }, + "API_CALLBACK": { + "TITLE": "Callback URL", + "SUBTITLE": "You have to configure the message callback URL in Bandwidth with the URL mentioned here." + } + } }, "WHATSAPP": { "TITLE": "WhatsApp Channel", diff --git a/app/javascript/dashboard/routes/dashboard/settings/campaigns/AddCampaign.vue b/app/javascript/dashboard/routes/dashboard/settings/campaigns/AddCampaign.vue index a71744939..e17960d9b 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/campaigns/AddCampaign.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/campaigns/AddCampaign.vue @@ -247,7 +247,7 @@ export default { if (this.isOngoingType) { return this.$store.getters['inboxes/getWebsiteInboxes']; } - return this.$store.getters['inboxes/getTwilioSMSInboxes']; + return this.$store.getters['inboxes/getSMSInboxes']; }, sendersAndBotList() { return [ diff --git a/app/javascript/dashboard/routes/dashboard/settings/campaigns/EditCampaign.vue b/app/javascript/dashboard/routes/dashboard/settings/campaigns/EditCampaign.vue index 6405e23dd..19a292e34 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/campaigns/EditCampaign.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/campaigns/EditCampaign.vue @@ -171,7 +171,7 @@ export default { if (this.isOngoingType) { return this.$store.getters['inboxes/getWebsiteInboxes']; } - return this.$store.getters['inboxes/getTwilioSMSInboxes']; + return this.$store.getters['inboxes/getSMSInboxes']; }, pageTitle() { return `${this.$t('CAMPAIGN.EDIT.TITLE')} - ${ diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/ChannelList.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/ChannelList.vue index 6f0121679..ac8518f16 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/ChannelList.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/ChannelList.vue @@ -50,7 +50,7 @@ export default { { key: 'facebook', name: 'Messenger' }, { key: 'twitter', name: 'Twitter' }, { key: 'whatsapp', name: 'WhatsApp' }, - { key: 'sms', name: 'SMS via Twilio' }, + { key: 'sms', name: 'SMS' }, { key: 'email', name: 'Email' }, { key: 'api', diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/FinishSetup.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/FinishSetup.vue index 95cbd7646..270c28fc6 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/FinishSetup.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/FinishSetup.vue @@ -29,6 +29,14 @@ >
+
+ + +
Whatsapp + + Sms + Email diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/BandwidthSms.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/BandwidthSms.vue new file mode 100644 index 000000000..dd531f951 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/BandwidthSms.vue @@ -0,0 +1,181 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Sms.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Sms.vue index cc4d18f1a..b669ad27d 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Sms.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Sms.vue @@ -4,18 +4,39 @@ :header-title="$t('INBOX_MGMT.ADD.SMS.TITLE')" :header-content="$t('INBOX_MGMT.ADD.SMS.DESC')" /> - +
+ +
+ +
diff --git a/app/javascript/dashboard/store/modules/inboxes.js b/app/javascript/dashboard/store/modules/inboxes.js index e266887f7..a180a5b80 100644 --- a/app/javascript/dashboard/store/modules/inboxes.js +++ b/app/javascript/dashboard/store/modules/inboxes.js @@ -78,9 +78,11 @@ export const getters = { item => item.channel_type === INBOX_TYPES.TWILIO ); }, - getTwilioSMSInboxes($state) { + getSMSInboxes($state) { return $state.records.filter( - item => item.channel_type === INBOX_TYPES.TWILIO && item.medium === 'sms' + item => + item.channel_type === INBOX_TYPES.SMS || + (item.channel_type === INBOX_TYPES.TWILIO && item.medium === 'sms') ); }, dialogFlowEnabledInboxes($state) { diff --git a/app/javascript/dashboard/store/modules/specs/inboxes/fixtures.js b/app/javascript/dashboard/store/modules/specs/inboxes/fixtures.js index 9db92b00a..f7b06d232 100644 --- a/app/javascript/dashboard/store/modules/specs/inboxes/fixtures.js +++ b/app/javascript/dashboard/store/modules/specs/inboxes/fixtures.js @@ -55,4 +55,11 @@ export default [ website_token: 'randomid125', enable_auto_assignment: true, }, + { + id: 6, + channel_id: 6, + name: 'Test Widget 6', + channel_type: 'Channel::Sms', + provider: 'default', + }, ]; diff --git a/app/javascript/dashboard/store/modules/specs/inboxes/getters.spec.js b/app/javascript/dashboard/store/modules/specs/inboxes/getters.spec.js index 73d6624e0..e8af7dd58 100644 --- a/app/javascript/dashboard/store/modules/specs/inboxes/getters.spec.js +++ b/app/javascript/dashboard/store/modules/specs/inboxes/getters.spec.js @@ -19,14 +19,14 @@ describe('#getters', () => { expect(getters.getTwilioInboxes(state).length).toEqual(1); }); - it('getTwilioSMSInboxes', () => { + it('getSMSInboxes', () => { const state = { records: inboxList }; - expect(getters.getTwilioSMSInboxes(state).length).toEqual(1); + expect(getters.getSMSInboxes(state).length).toEqual(2); }); it('dialogFlowEnabledInboxes', () => { const state = { records: inboxList }; - expect(getters.dialogFlowEnabledInboxes(state).length).toEqual(5); + expect(getters.dialogFlowEnabledInboxes(state).length).toEqual(6); }); it('getInbox', () => { diff --git a/app/javascript/shared/mixins/inboxMixin.js b/app/javascript/shared/mixins/inboxMixin.js index 022b9327e..aebbeebc1 100644 --- a/app/javascript/shared/mixins/inboxMixin.js +++ b/app/javascript/shared/mixins/inboxMixin.js @@ -8,6 +8,7 @@ export const INBOX_TYPES = { EMAIL: 'Channel::Email', TELEGRAM: 'Channel::Telegram', LINE: 'Channel::Line', + SMS: 'Channel::Sms', }; export default { diff --git a/app/jobs/send_reply_job.rb b/app/jobs/send_reply_job.rb index 08597a54d..cef80c9df 100644 --- a/app/jobs/send_reply_job.rb +++ b/app/jobs/send_reply_job.rb @@ -6,19 +6,20 @@ class SendReplyJob < ApplicationJob conversation = message.conversation channel_name = conversation.inbox.channel.class.to_s + services = { + 'Channel::TwitterProfile' => ::Twitter::SendOnTwitterService, + 'Channel::TwilioSms' => ::Twilio::SendOnTwilioService, + 'Channel::Line' => ::Line::SendOnLineService, + 'Channel::Telegram' => ::Telegram::SendOnTelegramService, + 'Channel::Whatsapp' => ::Whatsapp::SendOnWhatsappService, + 'Channel::Sms' => ::Sms::SendOnSmsService + } + case channel_name when 'Channel::FacebookPage' send_on_facebook_page(message) - when 'Channel::TwitterProfile' - ::Twitter::SendOnTwitterService.new(message: message).perform - when 'Channel::TwilioSms' - ::Twilio::SendOnTwilioService.new(message: message).perform - when 'Channel::Line' - ::Line::SendOnLineService.new(message: message).perform - when 'Channel::Telegram' - ::Telegram::SendOnTelegramService.new(message: message).perform - when 'Channel::Whatsapp' - ::Whatsapp::SendOnWhatsappService.new(message: message).perform + else + services[channel_name].new(message: message).perform if services[channel_name].present? end end diff --git a/app/jobs/webhooks/sms_events_job.rb b/app/jobs/webhooks/sms_events_job.rb new file mode 100644 index 000000000..c982e0da1 --- /dev/null +++ b/app/jobs/webhooks/sms_events_job.rb @@ -0,0 +1,13 @@ +class Webhooks::SmsEventsJob < ApplicationJob + queue_as :default + + def perform(params = {}) + return unless params[:type] == 'message-received' + + channel = Channel::Sms.find_by(phone_number: params[:to]) + return unless channel + + # TODO: pass to appropriate provider service from here + Sms::IncomingMessageService.new(inbox: channel.inbox, params: params[:message].with_indifferent_access).perform + end +end diff --git a/app/models/account.rb b/app/models/account.rb index 10e022aa4..da0dd062b 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -69,6 +69,7 @@ class Account < ApplicationRecord has_many :web_widgets, dependent: :destroy_async, class_name: '::Channel::WebWidget' has_many :webhooks, dependent: :destroy_async has_many :whatsapp_channels, dependent: :destroy_async, class_name: '::Channel::Whatsapp' + has_many :sms_channels, dependent: :destroy_async, class_name: '::Channel::Sms' has_many :working_hours, dependent: :destroy_async has_many :automation_rules, dependent: :destroy diff --git a/app/models/campaign.rb b/app/models/campaign.rb index a103eb44f..0093342d6 100644 --- a/app/models/campaign.rb +++ b/app/models/campaign.rb @@ -58,6 +58,7 @@ class Campaign < ApplicationRecord return if completed? Twilio::OneoffSmsCampaignService.new(campaign: self).perform if inbox.inbox_type == 'Twilio SMS' + Sms::OneoffSmsCampaignService.new(campaign: self).perform if inbox.inbox_type == 'Sms' end private @@ -69,14 +70,14 @@ class Campaign < ApplicationRecord def validate_campaign_inbox return unless inbox - errors.add :inbox, 'Unsupported Inbox type' unless ['Website', 'Twilio SMS'].include? inbox.inbox_type + errors.add :inbox, 'Unsupported Inbox type' unless ['Website', 'Twilio SMS', 'Sms'].include? inbox.inbox_type end # TO-DO we clean up with better validations when campaigns evolve into more inboxes def ensure_correct_campaign_attributes return if inbox.blank? - if inbox.inbox_type == 'Twilio SMS' + if ['Twilio SMS', 'Sms'].include?(inbox.inbox_type) self.campaign_type = 'one_off' self.scheduled_at ||= Time.now.utc else diff --git a/app/models/channel/sms.rb b/app/models/channel/sms.rb new file mode 100644 index 000000000..ff7dd2433 --- /dev/null +++ b/app/models/channel/sms.rb @@ -0,0 +1,81 @@ +# == Schema Information +# +# Table name: channel_sms +# +# id :bigint not null, primary key +# phone_number :string not null +# provider :string default("default") +# provider_config :jsonb +# created_at :datetime not null +# updated_at :datetime not null +# account_id :integer not null +# +# Indexes +# +# index_channel_sms_on_phone_number (phone_number) UNIQUE +# + +class Channel::Sms < ApplicationRecord + include Channelable + + self.table_name = 'channel_sms' + EDITABLE_ATTRS = [:phone_number, { provider_config: {} }].freeze + + validates :phone_number, presence: true, uniqueness: true + # before_save :validate_provider_config + + def name + 'Sms' + end + + # all this should happen in provider service . but hack mode on + def api_base_path + 'https://messaging.bandwidth.com/api/v2' + end + + # Extract later into provider Service + def send_message(phone_number, message) + if message.attachments.present? + send_attachment_message(phone_number, message) + else + send_text_message(phone_number, message.content) + end + end + + def send_text_message(contact_number, message) + response = HTTParty.post( + "#{api_base_path}/users/#{provider_config['account_id']}/messages", + basic_auth: bandwidth_auth, + headers: { 'Content-Type' => 'application/json' }, + body: { + 'to' => contact_number, + 'from' => phone_number, + 'text' => message, + 'applicationId' => provider_config['application_id'] + }.to_json + ) + + response.success? ? response.parsed_response['id'] : nil + end + + private + + def send_attachment_message(phone_number, message) + # fix me + end + + def bandwidth_auth + { username: provider_config['api_key'], password: provider_config['api_secret'] } + end + + # Extract later into provider Service + # let's revisit later + def validate_provider_config + response = HTTParty.post( + "#{api_base_path}/users/#{provider_config['account_id']}/messages", + basic_auth: bandwidth_auth, + headers: { 'Content-Type': 'application/json' } + ) + errors.add(:provider_config, 'error setting up') unless response.success? + end +end diff --git a/app/models/channel/whatsapp.rb b/app/models/channel/whatsapp.rb index 3642fac16..9349735fc 100644 --- a/app/models/channel/whatsapp.rb +++ b/app/models/channel/whatsapp.rb @@ -149,6 +149,6 @@ class Channel::Whatsapp < ApplicationRecord url: "#{ENV['FRONTEND_URL']}/webhooks/whatsapp/#{phone_number}" }.to_json ) - errors.add(:bot_token, 'error setting up the webook') unless response.success? + errors.add(:provider_config, 'error setting up the webook') unless response.success? end end diff --git a/app/models/inbox.rb b/app/models/inbox.rb index 774c66f9d..2d7077fa9 100644 --- a/app/models/inbox.rb +++ b/app/models/inbox.rb @@ -107,6 +107,8 @@ class Inbox < ApplicationRecord case channel_type when 'Channel::TwilioSms' "#{ENV['FRONTEND_URL']}/twilio/callback" + when 'Channel::Sms' + "#{ENV['FRONTEND_URL']}/webhooks/sms/#{channel.phone_number.delete_prefix('+')}" when 'Channel::Line' "#{ENV['FRONTEND_URL']}/webhooks/line/#{channel.line_channel_id}" end diff --git a/app/services/contacts/contactable_inboxes_service.rb b/app/services/contacts/contactable_inboxes_service.rb index fcd91d4c3..c5cde516f 100644 --- a/app/services/contacts/contactable_inboxes_service.rb +++ b/app/services/contacts/contactable_inboxes_service.rb @@ -14,6 +14,8 @@ class Contacts::ContactableInboxesService twilio_contactable_inbox(inbox) when 'Channel::Whatsapp' whatsapp_contactable_inbox(inbox) + when 'Channel::Sms' + sms_contactable_inbox(inbox) when 'Channel::Email' email_contactable_inbox(inbox) when 'Channel::Api' @@ -52,6 +54,12 @@ class Contacts::ContactableInboxesService { source_id: @contact.phone_number.delete('+'), inbox: inbox } end + def sms_contactable_inbox(inbox) + return unless @contact.phone_number + + { source_id: @contact.phone_number, inbox: inbox } + end + def twilio_contactable_inbox(inbox) return if @contact.phone_number.blank? diff --git a/app/services/sms/incoming_message_service.rb b/app/services/sms/incoming_message_service.rb new file mode 100644 index 000000000..62fda96ac --- /dev/null +++ b/app/services/sms/incoming_message_service.rb @@ -0,0 +1,66 @@ +class Sms::IncomingMessageService + include ::FileTypeHelper + + pattr_initialize [:inbox!, :params!] + + def perform + set_contact + set_conversation + @message = @conversation.messages.create( + content: params[:text], + account_id: @inbox.account_id, + inbox_id: @inbox.id, + message_type: :incoming, + sender: @contact, + source_id: params[:id] + ) + end + + private + + def account + @account ||= @inbox.account + end + + def phone_number + params[:from] + end + + def formatted_phone_number + TelephoneNumber.parse(phone_number).international_number + end + + def set_contact + contact_inbox = ::ContactBuilder.new( + source_id: params[:from], + inbox: @inbox, + contact_attributes: contact_attributes + ).perform + + @contact_inbox = contact_inbox + @contact = contact_inbox.contact + end + + def conversation_params + { + account_id: @inbox.account_id, + inbox_id: @inbox.id, + contact_id: @contact.id, + contact_inbox_id: @contact_inbox.id + } + end + + def set_conversation + @conversation = @contact_inbox.conversations.first + return if @conversation + + @conversation = ::Conversation.create!(conversation_params) + end + + def contact_attributes + { + name: formatted_phone_number, + phone_number: phone_number + } + end +end diff --git a/app/services/sms/oneoff_sms_campaign_service.rb b/app/services/sms/oneoff_sms_campaign_service.rb new file mode 100644 index 000000000..73a101d24 --- /dev/null +++ b/app/services/sms/oneoff_sms_campaign_service.rb @@ -0,0 +1,32 @@ +class Sms::OneoffSmsCampaignService + pattr_initialize [:campaign!] + + def perform + raise "Invalid campaign #{campaign.id}" if campaign.inbox.inbox_type != 'Sms' || !campaign.one_off? + raise 'Completed Campaign' if campaign.completed? + + # marks campaign completed so that other jobs won't pick it up + campaign.completed! + + audience_label_ids = campaign.audience.select { |audience| audience['type'] == 'Label' }.pluck('id') + audience_labels = campaign.account.labels.where(id: audience_label_ids).pluck(:title) + process_audience(audience_labels) + end + + private + + delegate :inbox, to: :campaign + delegate :channel, to: :inbox + + def process_audience(audience_labels) + campaign.account.contacts.tagged_with(audience_labels, any: true).each do |contact| + next if contact.phone_number.blank? + + send_message(to: contact.phone_number, content: campaign.message) + end + end + + def send_message(to:, content:) + channel.send_text_message(to, content) + end +end diff --git a/app/services/sms/send_on_sms_service.rb b/app/services/sms/send_on_sms_service.rb new file mode 100644 index 000000000..ccfede28c --- /dev/null +++ b/app/services/sms/send_on_sms_service.rb @@ -0,0 +1,16 @@ +class Sms::SendOnSmsService < Base::SendOnChannelService + private + + def channel_class + Channel::Sms + end + + def perform_reply + send_on_sms + end + + def send_on_sms + message_id = channel.send_message(message.conversation.contact_inbox.source_id, message) + message.update!(source_id: message_id) if message_id.present? + end +end diff --git a/config/routes.rb b/config/routes.rb index 49e7bf059..f90b6b84b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -271,6 +271,7 @@ Rails.application.routes.draw do post 'webhooks/line/:line_channel_id', to: 'webhooks/line#process_payload' post 'webhooks/telegram/:bot_token', to: 'webhooks/telegram#process_payload' post 'webhooks/whatsapp/:phone_number', to: 'webhooks/whatsapp#process_payload' + post 'webhooks/sms/:phone_number', to: 'webhooks/sms#process_payload' get 'webhooks/instagram', to: 'webhooks/instagram#verify' post 'webhooks/instagram', to: 'webhooks/instagram#events' diff --git a/db/migrate/20220129024443_add_sms_channel.rb b/db/migrate/20220129024443_add_sms_channel.rb new file mode 100644 index 000000000..c65019f7e --- /dev/null +++ b/db/migrate/20220129024443_add_sms_channel.rb @@ -0,0 +1,11 @@ +class AddSmsChannel < ActiveRecord::Migration[6.1] + def change + create_table :channel_sms do |t| + t.integer :account_id, null: false + t.string :phone_number, null: false, index: { unique: true } + t.string :provider, default: 'default' + t.jsonb :provider_config, default: {} + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 27da2435d..081dbdc3a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -228,6 +228,16 @@ ActiveRecord::Schema.define(version: 2022_01_31_081750) do t.index ["line_channel_id"], name: "index_channel_line_on_line_channel_id", unique: true end + create_table "channel_sms", force: :cascade do |t| + t.integer "account_id", null: false + t.string "phone_number", null: false + t.string "provider", default: "default" + t.jsonb "provider_config", default: {} + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["phone_number"], name: "index_channel_sms_on_phone_number", unique: true + end + create_table "channel_telegram", force: :cascade do |t| t.string "bot_name" t.integer "account_id", null: false diff --git a/spec/builders/contact_inbox_builder_spec.rb b/spec/builders/contact_inbox_builder_spec.rb index 40f19aba1..80658b5ac 100644 --- a/spec/builders/contact_inbox_builder_spec.rb +++ b/spec/builders/contact_inbox_builder_spec.rb @@ -99,6 +99,53 @@ describe ::ContactInboxBuilder do end end + describe 'sms inbox' do + let!(:sms_channel) { create(:channel_sms, account: account) } + let!(:sms_inbox) { create(:inbox, channel: sms_channel, account: account) } + + it 'does not create contact inbox when contact inbox already exists with the source id provided' do + existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number) + contact_inbox = described_class.new( + contact_id: contact.id, + inbox_id: sms_inbox.id, + source_id: contact.phone_number + ).perform + + expect(contact_inbox.id).to be(existing_contact_inbox.id) + end + + it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do + existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number) + contact_inbox = described_class.new( + contact_id: contact.id, + inbox_id: sms_inbox.id + ).perform + + expect(contact_inbox.id).to be(existing_contact_inbox.id) + end + + it 'creates a new contact inbox when different source id is provided' do + existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number) + contact_inbox = described_class.new( + contact_id: contact.id, + inbox_id: sms_inbox.id, + source_id: '+224213223422' + ).perform + + expect(contact_inbox.id).not_to be(existing_contact_inbox.id) + expect(contact_inbox.source_id).not_to be('+224213223422') + end + + it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do + contact_inbox = described_class.new( + contact_id: contact.id, + inbox_id: sms_inbox.id + ).perform + + expect(contact_inbox.source_id).not_to be(contact.phone_number) + end + end + describe 'email inbox' do let!(:email_channel) { create(:channel_email, account: account) } let!(:email_inbox) { create(:inbox, channel: email_channel, account: account) } diff --git a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb index 1c7082abc..8ccf667ac 100644 --- a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb @@ -309,6 +309,18 @@ RSpec.describe 'Inboxes API', type: :request do expect(response.body).to include('callback_webhook_url') end + it 'creates a sms inbox when administrator' do + post "/api/v1/accounts/#{account.id}/inboxes", + headers: admin.create_new_auth_token, + params: { name: 'Sms Inbox', + channel: { type: 'sms', phone_number: '+123456789', provider_config: { test: 'test' } } }, + as: :json + + expect(response).to have_http_status(:success) + expect(response.body).to include('Sms Inbox') + expect(response.body).to include('+123456789') + end + it 'creates the webwidget inbox that allow messages after conversation is resolved' do post "/api/v1/accounts/#{account.id}/inboxes", headers: admin.create_new_auth_token, diff --git a/spec/controllers/webhooks/sms_controller_spec.rb b/spec/controllers/webhooks/sms_controller_spec.rb new file mode 100644 index 000000000..0ce9bb94d --- /dev/null +++ b/spec/controllers/webhooks/sms_controller_spec.rb @@ -0,0 +1,12 @@ +require 'rails_helper' + +RSpec.describe 'Webhooks::SmsController', type: :request do + describe 'POST /webhooks/sms/{:phone_number}' do + it 'call the sms events job with the params' do + allow(Webhooks::SmsEventsJob).to receive(:perform_later) + expect(Webhooks::SmsEventsJob).to receive(:perform_later) + post '/webhooks/sms/123221321', params: { content: 'hello' } + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/factories/channel/channel_sms.rb b/spec/factories/channel/channel_sms.rb new file mode 100644 index 000000000..3eb5b499a --- /dev/null +++ b/spec/factories/channel/channel_sms.rb @@ -0,0 +1,16 @@ +FactoryBot.define do + factory :channel_sms, class: 'Channel::Sms' do + sequence(:phone_number) { |n| "+123456789#{n}1" } + account + provider_config do + { 'account_id' => '1', + 'application_id' => '1', + 'api_key' => '1', + 'api_secret' => '1' } + end + + after(:create) do |channel_sms| + create(:inbox, channel: channel_sms, account: channel_sms.account) + end + end +end diff --git a/spec/jobs/send_reply_job_spec.rb b/spec/jobs/send_reply_job_spec.rb index 6954bb3c3..03b249368 100644 --- a/spec/jobs/send_reply_job_spec.rb +++ b/spec/jobs/send_reply_job_spec.rb @@ -75,5 +75,14 @@ RSpec.describe SendReplyJob, type: :job do expect(process_service).to receive(:perform) described_class.perform_now(message.id) end + + it 'calls ::Sms::SendOnSmsService when its sms message' do + sms_channel = create(:channel_sms) + message = create(:message, conversation: create(:conversation, inbox: sms_channel.inbox)) + allow(::Sms::SendOnSmsService).to receive(:new).with(message: message).and_return(process_service) + expect(::Sms::SendOnSmsService).to receive(:new).with(message: message) + expect(process_service).to receive(:perform) + described_class.perform_now(message.id) + end end end diff --git a/spec/jobs/webhooks/sms_events_job_spec.rb b/spec/jobs/webhooks/sms_events_job_spec.rb new file mode 100644 index 000000000..927e8adaa --- /dev/null +++ b/spec/jobs/webhooks/sms_events_job_spec.rb @@ -0,0 +1,56 @@ +require 'rails_helper' + +RSpec.describe Webhooks::SmsEventsJob, type: :job do + subject(:job) { described_class.perform_later(params) } + + let!(:sms_channel) { create(:channel_sms) } + let!(:params) do + { + time: '2022-02-02T23:14:05.309Z', + type: 'message-received', + to: sms_channel.phone_number, + description: 'Incoming message received', + message: { + 'id': '3232420-2323-234324', + 'owner': sms_channel.phone_number, + 'applicationId': '2342349-324234d-32432432', + 'time': '2022-02-02T23:14:05.262Z', + 'segmentCount': 1, + 'direction': 'in', + 'to': [ + sms_channel.phone_number + ], + 'from': '+14234234234', + 'text': 'test message' + } + } + end + + it 'enqueues the job' do + expect { job }.to have_enqueued_job(described_class) + .with(params) + .on_queue('default') + end + + context 'when invalid params' do + it 'returns nil when no bot_token' do + expect(described_class.perform_now({})).to be_nil + end + + it 'returns nil when invalid type' do + expect(described_class.perform_now({ type: 'invalid' })).to be_nil + end + end + + context 'when valid params' do + it 'calls Sms::IncomingMessageService' do + process_service = double + allow(Sms::IncomingMessageService).to receive(:new).and_return(process_service) + allow(process_service).to receive(:perform) + expect(Sms::IncomingMessageService).to receive(:new).with(inbox: sms_channel.inbox, + params: params[:message].with_indifferent_access) + expect(process_service).to receive(:perform) + described_class.perform_now(params) + end + end +end diff --git a/spec/models/campaign_spec.rb b/spec/models/campaign_spec.rb index 4ea07cd75..54936ac2b 100644 --- a/spec/models/campaign_spec.rb +++ b/spec/models/campaign_spec.rb @@ -78,6 +78,27 @@ RSpec.describe Campaign, type: :model do end end + context 'when SMS campaign' do + let!(:sms_channel) { create(:channel_sms) } + let!(:sms_inbox) { create(:inbox, channel: sms_channel) } + let(:campaign) { build(:campaign, inbox: sms_inbox) } + + it 'only saves campaign type as oneoff and wont leave scheduled_at empty' do + campaign.campaign_type = 'ongoing' + campaign.save! + expect(campaign.reload.campaign_type).to eq 'one_off' + expect(campaign.scheduled_at.present?).to eq true + end + + it 'calls sms service on trigger!' do + sms_service = double + expect(Sms::OneoffSmsCampaignService).to receive(:new).with(campaign: campaign).and_return(sms_service) + expect(sms_service).to receive(:perform) + campaign.save! + campaign.trigger! + end + end + context 'when Website campaign' do let(:campaign) { build(:campaign) } diff --git a/spec/services/contacts/contactable_inboxes_service_spec.rb b/spec/services/contacts/contactable_inboxes_service_spec.rb index 860771fbe..3efc00b1a 100644 --- a/spec/services/contacts/contactable_inboxes_service_spec.rb +++ b/spec/services/contacts/contactable_inboxes_service_spec.rb @@ -15,8 +15,8 @@ describe Contacts::ContactableInboxesService do let!(:email_inbox) { create(:inbox, channel: email_channel, account: account) } let!(:api_channel) { create(:channel_api, account: account) } let!(:api_inbox) { create(:inbox, channel: api_channel, account: account) } - let!(:website_channel) { create(:channel_widget, account: account) } - let!(:website_inbox) { create(:inbox, channel: website_channel, account: account) } + let!(:website_inbox) { create(:inbox, channel: create(:channel_widget, account: account), account: account) } + let!(:sms_inbox) { create(:inbox, channel: create(:channel_sms, account: account), account: account) } describe '#get' do it 'returns the contactable inboxes for the contact' do @@ -25,7 +25,7 @@ describe Contacts::ContactableInboxesService do expect(contactable_inboxes).to include({ source_id: contact.phone_number, inbox: twilio_sms_inbox }) expect(contactable_inboxes).to include({ source_id: "whatsapp:#{contact.phone_number}", inbox: twilio_whatsapp_inbox }) expect(contactable_inboxes).to include({ source_id: contact.email, inbox: email_inbox }) - expect(contactable_inboxes.pluck(:inbox)).to include(api_inbox) + expect(contactable_inboxes).to include({ source_id: contact.phone_number, inbox: sms_inbox }) end it 'doest not return the non contactable inboxes for the contact' do diff --git a/spec/services/sms/incoming_message_service_spec.rb b/spec/services/sms/incoming_message_service_spec.rb new file mode 100644 index 000000000..1e86d8015 --- /dev/null +++ b/spec/services/sms/incoming_message_service_spec.rb @@ -0,0 +1,31 @@ +require 'rails_helper' + +describe Sms::IncomingMessageService do + describe '#perform' do + let!(:sms_channel) { create(:channel_sms) } + + context 'when valid text message params' do + it 'creates appropriate conversations, message and contacts' do + params = { + + 'id': '3232420-2323-234324', + 'owner': sms_channel.phone_number, + 'applicationId': '2342349-324234d-32432432', + 'time': '2022-02-02T23:14:05.262Z', + 'segmentCount': 1, + 'direction': 'in', + 'to': [ + sms_channel.phone_number + ], + 'from': '+14234234234', + 'text': 'test message' + + }.with_indifferent_access + described_class.new(inbox: sms_channel.inbox, params: params).perform + expect(sms_channel.inbox.conversations.count).not_to eq(0) + expect(Contact.all.first.name).to eq('+1 423-423-4234') + expect(sms_channel.inbox.messages.first.content).to eq('test message') + end + end + end +end diff --git a/spec/services/sms/oneoff_sms_campaign_service_spec.rb b/spec/services/sms/oneoff_sms_campaign_service_spec.rb new file mode 100644 index 000000000..9049175d8 --- /dev/null +++ b/spec/services/sms/oneoff_sms_campaign_service_spec.rb @@ -0,0 +1,47 @@ +require 'rails_helper' + +describe Sms::OneoffSmsCampaignService do + subject(:sms_campaign_service) { described_class.new(campaign: campaign) } + + let(:account) { create(:account) } + let!(:sms_channel) { create(:channel_sms) } + let!(:sms_inbox) { create(:inbox, channel: sms_channel) } + let(:label1) { create(:label, account: account) } + let(:label2) { create(:label, account: account) } + let!(:campaign) do + create(:campaign, inbox: sms_inbox, account: account, + audience: [{ type: 'Label', id: label1.id }, { type: 'Label', id: label2.id }]) + end + + describe 'perform' do + before do + stub_request(:post, 'https://messaging.bandwidth.com/api/v2/users/1/messages').to_return( + status: 200, + body: { 'id' => '1' }.to_json, + headers: {} + ) + end + + it 'raises error if the campaign is completed' do + campaign.completed! + + expect { sms_campaign_service.perform }.to raise_error 'Completed Campaign' + end + + it 'raises error invalid campaign when its not a oneoff sms campaign' do + campaign = create(:campaign) + + expect { described_class.new(campaign: campaign).perform }.to raise_error "Invalid campaign #{campaign.id}" + end + + it 'send messages to contacts in the audience and marks the campaign completed' do + contact_with_label1, contact_with_label2, contact_with_both_labels = FactoryBot.create_list(:contact, 3, :with_phone_number, account: account) + contact_with_label1.update_labels([label1.title]) + contact_with_label2.update_labels([label2.title]) + contact_with_both_labels.update_labels([label1.title, label2.title]) + sms_campaign_service.perform + assert_requested(:post, 'https://messaging.bandwidth.com/api/v2/users/1/messages', times: 3) + expect(campaign.reload.completed?).to eq true + end + end +end diff --git a/spec/services/sms/send_on_sms_service_spec.rb b/spec/services/sms/send_on_sms_service_spec.rb new file mode 100644 index 000000000..7304fad88 --- /dev/null +++ b/spec/services/sms/send_on_sms_service_spec.rb @@ -0,0 +1,28 @@ +require 'rails_helper' + +describe Sms::SendOnSmsService do + describe '#perform' do + context 'when a valid message' do + let(:sms_request) { double } + let!(:sms_channel) { create(:channel_sms) } + let!(:contact_inbox) { create(:contact_inbox, inbox: sms_channel.inbox, source_id: '+123456789') } + let!(:conversation) { create(:conversation, contact_inbox: contact_inbox, inbox: sms_channel.inbox) } + + it 'calls channel.send_message' do + message = create(:message, message_type: :outgoing, content: 'test', + conversation: conversation) + allow(HTTParty).to receive(:post).and_return(sms_request) + allow(sms_request).to receive(:success?).and_return(true) + allow(sms_request).to receive(:parsed_response).and_return({ 'id' => '123456789' }) + expect(HTTParty).to receive(:post).with( + 'https://messaging.bandwidth.com/api/v2/users/1/messages', + basic_auth: { username: '1', password: '1' }, + headers: { 'Content-Type' => 'application/json' }, + body: { 'to' => '+123456789', 'from' => sms_channel.phone_number, 'text' => 'test', 'applicationId' => '1' }.to_json + ) + described_class.new(message: message).perform + expect(message.reload.source_id).to eq('123456789') + end + end + end +end From 9454c6b14f75e778ef98cf84bdafdf0ed8ae5705 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Thu, 3 Feb 2022 18:25:28 -0800 Subject: [PATCH 058/101] Fix: Conversation filter permissions (#3908) fixes: chatwoot/product#225 --- app/finders/conversation_finder.rb | 2 +- spec/finders/conversation_finder_spec.rb | 28 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/app/finders/conversation_finder.rb b/app/finders/conversation_finder.rb index 3409a2450..58013f128 100644 --- a/app/finders/conversation_finder.rb +++ b/app/finders/conversation_finder.rb @@ -55,7 +55,7 @@ class ConversationFinder def set_inboxes @inbox_ids = if params[:inbox_id] - current_account.inboxes.where(id: params[:inbox_id]) + @current_user.assigned_inboxes.where(id: params[:inbox_id]) else @current_user.assigned_inboxes.pluck(:id) end diff --git a/spec/finders/conversation_finder_spec.rb b/spec/finders/conversation_finder_spec.rb index 67b526b64..906c0e3f2 100644 --- a/spec/finders/conversation_finder_spec.rb +++ b/spec/finders/conversation_finder_spec.rb @@ -6,7 +6,9 @@ describe ::ConversationFinder do let!(:account) { create(:account) } let!(:user_1) { create(:user, account: account) } let!(:user_2) { create(:user, account: account) } + let!(:admin) { create(:user, account: account, role: :administrator) } let!(:inbox) { create(:inbox, account: account, enable_auto_assignment: false) } + let!(:restricted_inbox) { create(:inbox, account: account) } before do create(:inbox_member, user: user_1, inbox: inbox) @@ -30,6 +32,32 @@ describe ::ConversationFinder do end end + context 'with inbox' do + let!(:restricted_conversation) { create(:conversation, account: account, inbox_id: restricted_inbox.id) } + + it 'returns conversation from any inbox if its admin' do + params = { inbox_id: restricted_inbox.id } + result = described_class.new(admin, params).perform + + expect(result[:conversations].map(&:id)).to include(restricted_conversation.id) + end + + it 'returns conversation from inbox if agent is its member' do + params = { inbox_id: restricted_inbox.id } + create(:inbox_member, user: user_1, inbox: restricted_inbox) + result = described_class.new(user_1, params).perform + + expect(result[:conversations].map(&:id)).to include(restricted_conversation.id) + end + + it 'does not return conversations from inboxes where agent is not a member' do + params = { inbox_id: restricted_inbox.id } + result = described_class.new(user_1, params).perform + + expect(result[:conversations].map(&:id)).not_to include(restricted_conversation.id) + end + end + context 'with assignee_type all' do let(:params) { { assignee_type: 'all' } } From 32673ea8b402fafb8a15c8639b11bc972b1b92dd Mon Sep 17 00:00:00 2001 From: Sagar Date: Fri, 4 Feb 2022 05:28:53 +0100 Subject: [PATCH 059/101] chore: Allow Self Hosted Instances to use Support Inbox within Dashboard for Agents (#3907) --- app/views/layouts/vueapp.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/vueapp.html.erb b/app/views/layouts/vueapp.html.erb index d20de94a7..f7d7155d1 100644 --- a/app/views/layouts/vueapp.html.erb +++ b/app/views/layouts/vueapp.html.erb @@ -67,7 +67,7 @@ + + diff --git a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue index 93c47ebf6..44dac4d53 100644 --- a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue +++ b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue @@ -1,56 +1,29 @@ 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 6b26f94b9..449b0383f 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue @@ -12,8 +12,11 @@ v-if="filterItemsList" :type="type" :filter-items-list="filterItemsList" + :group-by-filter-items-list="groupByfilterItemsList" + :selected-group-by-filter="selectedGroupByFilter" @date-range-change="onDateRangeChange" @filter-change="onFilterChange" + @group-by-filter-change="onGroupByFilterChange" />
@@ -51,6 +54,7 @@ import ReportFilters from './ReportFilters'; import fromUnixTime from 'date-fns/fromUnixTime'; import format from 'date-fns/format'; +import { GROUP_BY_FILTER } from '../constants'; const REPORTS_KEYS = { CONVERSATIONS: 'conversations_count', @@ -88,6 +92,9 @@ export default { to: 0, currentSelection: 0, selectedFilter: null, + groupBy: GROUP_BY_FILTER[1], + groupByfilterItemsList: this.$t('REPORT.GROUP_BY_DAY_OPTIONS'), + selectedGroupByFilter: null, }; }, computed: { @@ -105,9 +112,28 @@ export default { return {}; } if (!this.accountReport.data.length) return {}; - const labels = this.accountReport.data.map(element => - format(fromUnixTime(element.timestamp), 'dd/MMM') - ); + const labels = this.accountReport.data.map(element => { + if (this.groupBy.period === GROUP_BY_FILTER[2].period) { + let week_date = new Date(fromUnixTime(element.timestamp)); + const first_day = week_date.getDate() - week_date.getDay(); + const last_day = first_day + 6; + + const week_first_date = new Date(week_date.setDate(first_day)); + const week_last_date = new Date(week_date.setDate(last_day)); + + return `${format(week_first_date, 'dd/MM/yy')} - ${format( + week_last_date, + 'dd/MM/yy' + )}`; + } + if (this.groupBy.period === GROUP_BY_FILTER[3].period) { + return format(fromUnixTime(element.timestamp), 'MMM-yyyy'); + } + if (this.groupBy.period === GROUP_BY_FILTER[4].period) { + return format(fromUnixTime(element.timestamp), 'yyyy'); + } + return format(fromUnixTime(element.timestamp), 'dd-MMM-yyyy'); + }); const data = this.accountReport.data.map(element => element.value); return { labels, @@ -148,24 +174,26 @@ export default { methods: { fetchAllData() { if (this.selectedFilter) { - const { from, to } = this; + const { from, to, groupBy } = this; this.$store.dispatch('fetchAccountSummary', { from, to, type: this.type, id: this.selectedFilter.id, + groupBy: groupBy.period, }); this.fetchChartData(); } }, fetchChartData() { - const { from, to } = this; + const { from, to, groupBy } = this; this.$store.dispatch('fetchAccountReport', { metric: this.metrics[this.currentSelection].KEY, from, to, type: this.type, id: this.selectedFilter.id, + groupBy: groupBy.period, }); }, downloadReports() { @@ -195,9 +223,19 @@ export default { this.currentSelection = index; this.fetchChartData(); }, - onDateRangeChange({ from, to }) { + onDateRangeChange({ from, to, groupBy }) { this.from = from; this.to = to; + this.groupByfilterItemsList = this.fetchFilterItems(groupBy); + const filterItems = this.groupByfilterItemsList.filter( + item => item.id === this.groupBy.id + ); + if (filterItems.length > 0) { + this.selectedGroupByFilter = filterItems[0]; + } else { + this.selectedGroupByFilter = this.groupByfilterItemsList[0]; + this.groupBy = GROUP_BY_FILTER[this.selectedGroupByFilter.id]; + } this.fetchAllData(); }, onFilterChange(payload) { @@ -206,6 +244,22 @@ export default { this.fetchAllData(); } }, + onGroupByFilterChange(payload) { + this.groupBy = GROUP_BY_FILTER[payload.id]; + this.fetchAllData(); + }, + fetchFilterItems(group_by) { + switch (group_by) { + case GROUP_BY_FILTER[2].period: + return this.$t('REPORT.GROUP_BY_WEEK_OPTIONS'); + case GROUP_BY_FILTER[3].period: + return this.$t('REPORT.GROUP_BY_MONTH_OPTIONS'); + case GROUP_BY_FILTER[4].period: + return this.$t('REPORT.GROUP_BY_YEAR_OPTIONS'); + default: + return this.$t('REPORT.GROUP_BY_DAY_OPTIONS'); + } + }, }, }; diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/constants.js b/app/javascript/dashboard/routes/dashboard/settings/reports/constants.js new file mode 100644 index 000000000..f285061cb --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/constants.js @@ -0,0 +1,6 @@ +export const GROUP_BY_FILTER = { + 1: { id: 1, period: 'day' }, + 2: { id: 2, period: 'week' }, + 3: { id: 3, period: 'month' }, + 4: { id: 4, period: 'year' }, +}; diff --git a/app/javascript/dashboard/store/modules/reports.js b/app/javascript/dashboard/store/modules/reports.js index 87050bcef..027b2388a 100644 --- a/app/javascript/dashboard/store/modules/reports.js +++ b/app/javascript/dashboard/store/modules/reports.js @@ -43,7 +43,8 @@ export const actions = { reportObj.from, reportObj.to, reportObj.type, - reportObj.id + reportObj.id, + reportObj.groupBy ).then(accountReport => { let { data } = accountReport; data = data.filter( @@ -68,7 +69,8 @@ export const actions = { reportObj.from, reportObj.to, reportObj.type, - reportObj.id + reportObj.id, + reportObj.groupBy ) .then(accountSummary => { commit(types.default.SET_ACCOUNT_SUMMARY, accountSummary.data); diff --git a/config/locales/en.yml b/config/locales/en.yml index 0804fbcd9..c52b660e8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -57,6 +57,7 @@ en: conversations_count: Conversations count avg_first_response_time: Avg first response time (Minutes) avg_resolution_time: Avg resolution time (Minutes) + default_group_by: day notifications: notification_title: diff --git a/spec/builders/v2/report_builder_spec.rb b/spec/builders/v2/report_builder_spec.rb index bcf02f1d8..16fe18f49 100644 --- a/spec/builders/v2/report_builder_spec.rb +++ b/spec/builders/v2/report_builder_spec.rb @@ -147,6 +147,18 @@ describe ::V2::ReportBuilder do expect(metrics[:avg_resolution_time]).to be 0 expect(metrics[:resolutions_count]).to be 0 end + + it 'returns argument error for incorrect group by' do + params = { + type: :account, + since: (Time.zone.today - 3.days).to_time.to_i.to_s, + until: Time.zone.today.to_time.to_i.to_s, + group_by: 'test'.to_s + } + + builder = V2::ReportBuilder.new(account, params) + expect { builder.summary }.to raise_error(ArgumentError) + end end context 'when report type is label' do @@ -247,6 +259,38 @@ describe ::V2::ReportBuilder do expect(metrics[:avg_resolution_time]).to be 0 expect(metrics[:resolutions_count]).to be 0 end + + it 'returns summary for correct group by' do + params = { + type: :label, + id: label_2.id, + since: (Time.zone.today - 3.days).to_time.to_i.to_s, + until: Time.zone.today.to_time.to_i.to_s, + group_by: 'week'.to_s + } + + builder = V2::ReportBuilder.new(account, params) + metrics = builder.summary + + expect(metrics[:conversations_count]).to be 5 + expect(metrics[:incoming_messages_count]).to be 5 + expect(metrics[:outgoing_messages_count]).to be 15 + expect(metrics[:avg_resolution_time]).to be 0 + expect(metrics[:resolutions_count]).to be 0 + end + + it 'returns argument error for incorrect group by' do + params = { + type: :label, + id: label_2.id, + since: (Time.zone.today - 3.days).to_time.to_i.to_s, + until: Time.zone.today.to_time.to_i.to_s, + group_by: 'test'.to_s + } + + builder = V2::ReportBuilder.new(account, params) + expect { builder.summary }.to raise_error(ArgumentError) + end end end end From 464e12ceb726dea5562ed4a110cf6c47833c5b4e Mon Sep 17 00:00:00 2001 From: "Aswin Dev P.S" Date: Tue, 15 Feb 2022 03:41:28 -0800 Subject: [PATCH 096/101] fix: Update auto reply and hide reply time for email inbox (#3985) Co-authored-by: Pranav Raj S --- .../dashboard/settings/inbox/Settings.vue | 2 +- app/models/message.rb | 14 ++++----- app/workers/email_reply_worker.rb | 3 +- spec/models/message_spec.rb | 29 +++++++++++++++++++ 4 files changed, 38 insertions(+), 10 deletions(-) diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue index 645a0990a..31f435a82 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue @@ -149,7 +149,7 @@ :richtext="!textAreaChannels" />
-