diff --git a/Gemfile.lock b/Gemfile.lock index a16b50a98..2222403f1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,63 +9,63 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (6.1.4.6) - actionpack (= 6.1.4.6) - activesupport (= 6.1.4.6) + actioncable (6.1.4.7) + actionpack (= 6.1.4.7) + activesupport (= 6.1.4.7) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.4.6) - actionpack (= 6.1.4.6) - activejob (= 6.1.4.6) - activerecord (= 6.1.4.6) - activestorage (= 6.1.4.6) - activesupport (= 6.1.4.6) + actionmailbox (6.1.4.7) + actionpack (= 6.1.4.7) + activejob (= 6.1.4.7) + activerecord (= 6.1.4.7) + activestorage (= 6.1.4.7) + activesupport (= 6.1.4.7) mail (>= 2.7.1) - actionmailer (6.1.4.6) - actionpack (= 6.1.4.6) - actionview (= 6.1.4.6) - activejob (= 6.1.4.6) - activesupport (= 6.1.4.6) + actionmailer (6.1.4.7) + actionpack (= 6.1.4.7) + actionview (= 6.1.4.7) + activejob (= 6.1.4.7) + activesupport (= 6.1.4.7) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.4.6) - actionview (= 6.1.4.6) - activesupport (= 6.1.4.6) + actionpack (6.1.4.7) + actionview (= 6.1.4.7) + activesupport (= 6.1.4.7) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.4.6) - actionpack (= 6.1.4.6) - activerecord (= 6.1.4.6) - activestorage (= 6.1.4.6) - activesupport (= 6.1.4.6) + actiontext (6.1.4.7) + actionpack (= 6.1.4.7) + activerecord (= 6.1.4.7) + activestorage (= 6.1.4.7) + activesupport (= 6.1.4.7) nokogiri (>= 1.8.5) - actionview (6.1.4.6) - activesupport (= 6.1.4.6) + actionview (6.1.4.7) + activesupport (= 6.1.4.7) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) active_record_query_trace (1.8) - activejob (6.1.4.6) - activesupport (= 6.1.4.6) + activejob (6.1.4.7) + activesupport (= 6.1.4.7) globalid (>= 0.3.6) - activemodel (6.1.4.6) - activesupport (= 6.1.4.6) - activerecord (6.1.4.6) - activemodel (= 6.1.4.6) - activesupport (= 6.1.4.6) + activemodel (6.1.4.7) + activesupport (= 6.1.4.7) + activerecord (6.1.4.7) + activemodel (= 6.1.4.7) + activesupport (= 6.1.4.7) activerecord-import (1.3.0) activerecord (>= 4.2) - activestorage (6.1.4.6) - actionpack (= 6.1.4.6) - activejob (= 6.1.4.6) - activerecord (= 6.1.4.6) - activesupport (= 6.1.4.6) + activestorage (6.1.4.7) + actionpack (= 6.1.4.7) + activejob (= 6.1.4.7) + activerecord (= 6.1.4.7) + activesupport (= 6.1.4.7) marcel (~> 1.0.0) mini_mime (>= 1.1.0) - activesupport (6.1.4.6) + activesupport (6.1.4.7) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -419,29 +419,29 @@ GEM rack-test (1.1.0) rack (>= 1.0, < 3) rack-timeout (0.6.0) - rails (6.1.4.6) - actioncable (= 6.1.4.6) - actionmailbox (= 6.1.4.6) - actionmailer (= 6.1.4.6) - actionpack (= 6.1.4.6) - actiontext (= 6.1.4.6) - actionview (= 6.1.4.6) - activejob (= 6.1.4.6) - activemodel (= 6.1.4.6) - activerecord (= 6.1.4.6) - activestorage (= 6.1.4.6) - activesupport (= 6.1.4.6) + rails (6.1.4.7) + actioncable (= 6.1.4.7) + actionmailbox (= 6.1.4.7) + actionmailer (= 6.1.4.7) + actionpack (= 6.1.4.7) + actiontext (= 6.1.4.7) + actionview (= 6.1.4.7) + activejob (= 6.1.4.7) + activemodel (= 6.1.4.7) + activerecord (= 6.1.4.7) + activestorage (= 6.1.4.7) + activesupport (= 6.1.4.7) bundler (>= 1.15.0) - railties (= 6.1.4.6) + railties (= 6.1.4.7) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) rails-html-sanitizer (1.4.2) loofah (~> 2.3) - railties (6.1.4.6) - actionpack (= 6.1.4.6) - activesupport (= 6.1.4.6) + railties (6.1.4.7) + actionpack (= 6.1.4.7) + activesupport (= 6.1.4.7) method_source rake (>= 0.13) thor (~> 1.0) @@ -574,7 +574,7 @@ GEM spring-watcher-listen (2.0.1) listen (>= 2.7, < 4.0) spring (>= 1.2, < 3.0) - sprockets (4.0.2) + sprockets (4.0.3) concurrent-ruby (~> 1.0) rack (> 1, < 3) sprockets-rails (3.4.2) @@ -751,4 +751,4 @@ RUBY VERSION ruby 3.0.2p107 BUNDLED WITH - 2.2.25 + 2.3.8 diff --git a/app/builders/messages/messenger/message_builder.rb b/app/builders/messages/messenger/message_builder.rb index 08aa58be0..2e58825f4 100644 --- a/app/builders/messages/messenger/message_builder.rb +++ b/app/builders/messages/messenger/message_builder.rb @@ -1,10 +1,14 @@ class Messages::Messenger::MessageBuilder + include ::FileTypeHelper + def process_attachment(attachment) return if attachment['type'].to_sym == :template attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url)) attachment_obj.save! attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url] + fetch_story_link(attachment_obj) if attachment_obj.file_type == 'story_mention' + update_attachment_file_type(attachment_obj) end def attach_file(attachment, file_url) @@ -22,7 +26,7 @@ class Messages::Messenger::MessageBuilder file_type = attachment['type'].to_sym params = { file_type: file_type, account_id: @message.account_id } - if [:image, :file, :audio, :video].include? file_type + if [:image, :file, :audio, :video, :share, :story_mention].include? file_type params.merge!(file_type_params(attachment)) elsif file_type == :location params.merge!(location_params(attachment)) @@ -39,4 +43,31 @@ class Messages::Messenger::MessageBuilder remote_file_url: attachment['payload']['url'] } end + + def update_attachment_file_type(attachment) + return unless attachment.file_type == 'share' || attachment.file_type == 'story_mention' + + attachment.file_type = file_type(attachment.file&.content_type) + attachment.save! + end + + def fetch_story_link(attachment) + message = attachment.message + begin + k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook? + result = k.get_object(message.source_id, fields: %w[story from]) || {} + rescue Koala::Facebook::AuthenticationError + @inbox.channel.authorization_error! + raise + rescue StandardError => e + result = {} + Sentry.capture_exception(e) + end + story_id = result['story']['mention']['id'] + story_sender = result['from']['username'] + message.content_attributes[:story_sender] = story_sender + message.content_attributes[:story_id] = story_id + message.content = I18n.t('conversations.messages.instagram_story_content', story_sender: story_sender) + message.save! + end end diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 2bda5c07a..d60e4c3e8 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -49,7 +49,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController return if permitted_params(channel_attributes)[:channel].blank? validate_email_channel(channel_attributes) if @inbox.inbox_type == 'Email' - @inbox.channel.update!(permitted_params(channel_attributes)[:channel]) update_channel_feature_flags end diff --git a/app/controllers/api/v1/widget/base_controller.rb b/app/controllers/api/v1/widget/base_controller.rb index 38c880526..ad555fc22 100644 --- a/app/controllers/api/v1/widget/base_controller.rb +++ b/app/controllers/api/v1/widget/base_controller.rb @@ -68,7 +68,7 @@ class Api::V1::Widget::BaseController < ApplicationController mergee_contact: @contact ).perform else - @contact.update!(email: email, name: contact_name) + @contact.update!(email: email, name: contact_name, phone_number: contact_phone_number) end end @@ -80,6 +80,10 @@ class Api::V1::Widget::BaseController < ApplicationController params[:contact][:name] || contact_email.split('@')[0] end + def contact_phone_number + params[:contact][:phone_number] + end + def browser_params { browser_name: browser.name, diff --git a/app/controllers/api/v1/widget/conversations_controller.rb b/app/controllers/api/v1/widget/conversations_controller.rb index 8d28345d4..8cfb65be6 100644 --- a/app/controllers/api/v1/widget/conversations_controller.rb +++ b/app/controllers/api/v1/widget/conversations_controller.rb @@ -51,6 +51,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController end def permitted_params - params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id]) + params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email, :phone_number], + message: [:content, :referer_url, :timestamp, :echo_id]) end end diff --git a/app/controllers/api/v2/accounts/reports_controller.rb b/app/controllers/api/v2/accounts/reports_controller.rb index a83611676..efa09a43c 100644 --- a/app/controllers/api/v2/accounts/reports_controller.rb +++ b/app/controllers/api/v2/accounts/reports_controller.rb @@ -41,12 +41,22 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController raise Pundit::NotAuthorizedError unless Current.account_user.administrator? end - def summary_params + def current_summary_params { type: params[:type].to_sym, - since: params[:since], - until: params[:until], id: params[:id], + since: range[:current][:since], + until: range[:current][:until], + group_by: params[:group_by] + } + end + + def previous_summary_params + { + type: params[:type].to_sym, + id: params[:id], + since: range[:previous][:since], + until: range[:previous][:until], group_by: params[:group_by] } end @@ -63,8 +73,22 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController } end + def range + { + current: { + since: params[:since], + until: params[:until] + }, + previous: { + since: (params[:since].to_i - (params[:until].to_i - params[:since].to_i)).to_s, + until: params[:since] + } + } + end + def summary_metrics - builder = V2::ReportBuilder.new(Current.account, summary_params) - builder.summary + summary = V2::ReportBuilder.new(Current.account, current_summary_params).summary + summary[:previous] = V2::ReportBuilder.new(Current.account, previous_summary_params).summary + summary end end diff --git a/app/helpers/file_type_helper.rb b/app/helpers/file_type_helper.rb index 9e822b6c4..db3f6249d 100644 --- a/app/helpers/file_type_helper.rb +++ b/app/helpers/file_type_helper.rb @@ -14,7 +14,8 @@ module FileTypeHelper 'image/png', 'image/gif', 'image/bmp', - 'image/webp' + 'image/webp', + 'image' ].include?(content_type) end @@ -23,7 +24,8 @@ module FileTypeHelper 'video/ogg', 'video/mp4', 'video/webm', - 'video/quicktime' + 'video/quicktime', + 'video' ].include?(content_type) end end diff --git a/app/javascript/dashboard/assets/scss/widgets/_report.scss b/app/javascript/dashboard/assets/scss/widgets/_report.scss index c62eba70e..6e8b2fe5c 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_report.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_report.scss @@ -20,12 +20,30 @@ color: $color-heading; } + .metric-wrap { + align-items: baseline; + display: flex; + } + .metric { font-size: $font-size-big; font-weight: $font-weight-feather; margin-top: $space-smaller; } + .metric-trend { + font-size: $font-size-small; + margin-left: $space-small; + } + + .metric-up { + color: $success-color; + } + + .metric-down { + color: $alert-color; + } + .desc { @include margin($zero); font-size: $font-size-small; diff --git a/app/javascript/dashboard/components/buttons/ToggleButton.vue b/app/javascript/dashboard/components/buttons/ToggleButton.vue new file mode 100644 index 000000000..37328aba5 --- /dev/null +++ b/app/javascript/dashboard/components/buttons/ToggleButton.vue @@ -0,0 +1,65 @@ + + + + diff --git a/app/javascript/dashboard/components/layout/Sidebar.vue b/app/javascript/dashboard/components/layout/Sidebar.vue index ee64f4949..cdd338710 100644 --- a/app/javascript/dashboard/components/layout/Sidebar.vue +++ b/app/javascript/dashboard/components/layout/Sidebar.vue @@ -19,23 +19,6 @@ :current-role="currentRole" @add-label="showAddLabelPopup" /> - - - - - - @@ -46,12 +29,8 @@ import adminMixin from '../../mixins/isAdmin'; import { getSidebarItems } from './config/default-sidebar'; import alertMixin from 'shared/mixins/alertMixin'; -import AccountSelector from './sidebarComponents/AccountSelector.vue'; -import AddAccountModal from './sidebarComponents/AddAccountModal.vue'; -import AddLabelModal from '../../routes/dashboard/settings/labels/AddLabel'; import PrimarySidebar from './sidebarComponents/Primary'; import SecondarySidebar from './sidebarComponents/Secondary'; -import WootKeyShortcutModal from 'components/widgets/modal/WootKeyShortcutModal'; import { hasPressedAltAndCKey, hasPressedAltAndRKey, @@ -65,21 +44,13 @@ import router from '../../routes'; export default { components: { - AccountSelector, - AddAccountModal, - AddLabelModal, PrimarySidebar, SecondarySidebar, - WootKeyShortcutModal, }, mixins: [adminMixin, alertMixin, eventListenerMixins], data() { return { showOptionsMenu: false, - showAccountModal: false, - showCreateAccountModal: false, - showAddLabelModal: false, - showShortcutModal: false, }; }, @@ -162,10 +133,10 @@ export default { } }, toggleKeyShortcutModal() { - this.showShortcutModal = true; + this.$emit('open-key-shortcut-modal'); }, closeKeyShortcutModal() { - this.showShortcutModal = false; + this.$emit('close-key-shortcut-modal'); }, handleKeyEvents(e) { if (hasPressedCommandAndForwardSlash(e)) { @@ -200,20 +171,10 @@ export default { window.$chatwoot.toggle(); }, toggleAccountModal() { - this.showAccountModal = !this.showAccountModal; - }, - openCreateAccountModal() { - this.showAccountModal = false; - this.showCreateAccountModal = true; - }, - closeCreateAccountModal() { - this.showCreateAccountModal = false; + this.$emit('toggle-account-modal'); }, showAddLabelPopup() { - this.showAddLabelModal = true; - }, - hideAddLabelPopup() { - this.showAddLabelModal = false; + this.$emit('show-add-label-popup'); }, }, }; diff --git a/app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js b/app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js index 9e9e3a844..07792c5d5 100644 --- a/app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js +++ b/app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js @@ -69,7 +69,22 @@ export const OPERATOR_TYPES_4 = [ label: 'Is greater than', }, { - value: 'is_lesser_than', - label: 'Is lesser than', + value: 'is_less_than', + label: 'Is less than', + }, +]; + +export const OPERATOR_TYPES_5 = [ + { + value: 'is_greater_than', + label: 'Is greater than', + }, + { + value: 'is_less_than', + label: 'Is less than', + }, + { + value: 'days_before', + label: 'Is x days before', }, ]; diff --git a/app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.spec.js b/app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.spec.js new file mode 100644 index 000000000..4e8e57b16 --- /dev/null +++ b/app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.spec.js @@ -0,0 +1,15 @@ +import { + OPERATOR_TYPES_1, + OPERATOR_TYPES_2, + OPERATOR_TYPES_3, + OPERATOR_TYPES_4, +} from './FilterOperatorTypes'; + +describe('#filterOperators', () => { + it('Matches the correct Operators', () => { + expect(OPERATOR_TYPES_1).toMatchObject(OPERATOR_TYPES_1); + expect(OPERATOR_TYPES_2).toMatchObject(OPERATOR_TYPES_2); + expect(OPERATOR_TYPES_3).toMatchObject(OPERATOR_TYPES_3); + expect(OPERATOR_TYPES_4).toMatchObject(OPERATOR_TYPES_4); + }); +}); diff --git a/app/javascript/dashboard/components/widgets/FilterInput/Index.vue b/app/javascript/dashboard/components/widgets/FilterInput/Index.vue index d26467ab6..25203642a 100644 --- a/app/javascript/dashboard/components/widgets/FilterInput/Index.vue +++ b/app/javascript/dashboard/components/widgets/FilterInput/Index.vue @@ -141,6 +141,10 @@ export default { type: String, default: 'plain_text', }, + dataType: { + type: String, + default: 'plain_text', + }, operators: { type: Array, default: () => [], diff --git a/app/javascript/dashboard/components/widgets/ReportStatsCard.vue b/app/javascript/dashboard/components/widgets/ReportStatsCard.vue index 309627220..9bc46d021 100644 --- a/app/javascript/dashboard/components/widgets/ReportStatsCard.vue +++ b/app/javascript/dashboard/components/widgets/ReportStatsCard.vue @@ -7,9 +7,12 @@

{{ heading }}

-

- {{ point }} -

+
+

+ {{ point }} +

+ {{ trendValue }} +

{{ desc }}

@@ -20,10 +23,27 @@ export default { props: { heading: { type: String, default: '' }, point: { type: [Number, String], default: '' }, + trend: { type: Number, default: null }, index: { type: Number, default: null }, desc: { type: String, default: '' }, selected: Boolean, onClick: { type: Function, default: () => {} }, }, + computed: { + trendClass() { + if (this.trend > 0) { + return 'metric-trend metric-up'; + } + + return 'metric-trend metric-down'; + }, + trendValue() { + if (this.trend > 0) { + return `+${this.trend}%`; + } + + return `${this.trend}%`; + }, + }, }; diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationAdvancedFilter.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationAdvancedFilter.vue index 916df9a31..34a349fff 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationAdvancedFilter.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationAdvancedFilter.vue @@ -10,7 +10,12 @@ :key="i" v-model="appliedFilters[i]" :filter-groups="filterGroups" - :input-type="getInputType(appliedFilters[i].attribute_key)" + :input-type=" + getInputType( + appliedFilters[i].attribute_key, + appliedFilters[i].filter_operator + ) + " :operators="getOperators(appliedFilters[i].attribute_key)" :dropdown-values="getDropdownValues(appliedFilters[i].attribute_key)" :show-query-operator="i !== appliedFilters.length - 1" @@ -56,6 +61,7 @@ import { mapGetters } from 'vuex'; import { filterAttributeGroups } from './advancedFilterItems'; import filterMixin from 'shared/mixins/filterMixin'; import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js'; + export default { components: { FilterInputBox, @@ -76,6 +82,12 @@ export default { required, $each: { values: { + ensureBetween0to999(value, prop) { + if (prop.filter_operator === 'days_before') { + return parseInt(value, 10) > 0 && parseInt(value, 10) < 999; + } + return true; + }, required: requiredIf(prop => { return !( prop.filter_operator === 'is_present' || @@ -155,7 +167,9 @@ export default { const type = this.filterTypes.find(filter => filter.attributeKey === key); return type.attributeModel; }, - getInputType(key) { + getInputType(key, operator) { + if (key === 'created_at' || key === 'last_activity_at') + if (operator === 'days_before') return 'plain_text'; const type = this.filterTypes.find(filter => filter.attributeKey === key); return type.inputType; }, diff --git a/app/javascript/dashboard/components/widgets/conversation/Message.vue b/app/javascript/dashboard/components/widgets/conversation/Message.vue index 145660c07..51d82ff8c 100644 --- a/app/javascript/dashboard/components/widgets/conversation/Message.vue +++ b/app/javascript/dashboard/components/widgets/conversation/Message.vue @@ -50,7 +50,10 @@
  • @@ -64,6 +65,7 @@ class="message--unread" :data="message" :is-a-tweet="isATweet" + :has-instagram-story="hasInstagramStory" />
    { + it('Matches the correct filterItems', () => { + expect(defaultFilters).toMatchObject(defaultFilters); + expect(filterAttributeGroups).toMatchObject(filterAttributeGroups); + }); +}); diff --git a/app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/index.js b/app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/index.js index 98e27c5bb..a3e8e2c10 100644 --- a/app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/index.js +++ b/app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/index.js @@ -2,6 +2,7 @@ import { OPERATOR_TYPES_1, OPERATOR_TYPES_2, OPERATOR_TYPES_3, + OPERATOR_TYPES_5, } from '../../FilterInput/FilterOperatorTypes'; const filterTypes = [ @@ -85,6 +86,30 @@ const filterTypes = [ filterOperators: OPERATOR_TYPES_3, attributeModel: 'additional', }, + { + attributeKey: 'created_at', + attributeI18nKey: 'CREATED_AT', + inputType: 'date', + dataType: 'text', + filterOperators: OPERATOR_TYPES_5, + attributeModel: 'standard', + }, + { + attributeKey: 'last_activity_at', + attributeI18nKey: 'LAST_ACTIVITY', + inputType: 'date', + dataType: 'text', + filterOperators: OPERATOR_TYPES_5, + attributeModel: 'standard', + }, + { + attributeKey: 'referer', + attributeI18nKey: 'REFERER_LINK', + inputType: 'plain_text', + dataType: 'text', + filterOperators: OPERATOR_TYPES_5, + attributeModel: 'standard', + }, ]; export const filterAttributeGroups = [ @@ -120,6 +145,14 @@ export const filterAttributeGroups = [ key: 'labels', i18nKey: 'LABELS', }, + { + key: 'created_at', + i18nKey: 'CREATED_AT', + }, + { + key: 'last_activity_at', + i18nKey: 'LAST_ACTIVITY', + }, ], }, { diff --git a/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue b/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue index 957402f78..a20f3d34c 100644 --- a/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue +++ b/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue @@ -35,6 +35,19 @@ size="16" /> + + + { + if (!this.accountSummary.previous[metric_key]) return 0; + return Math.round( + ((this.accountSummary[metric_key] - + this.accountSummary.previous[metric_key]) / + this.accountSummary.previous[metric_key]) * + 100 + ); + }; + }, + displayMetric() { + return metric_key => { + if ( + ['avg_first_response_time', 'avg_resolution_time'].includes( + metric_key + ) + ) { + return formatTime(this.accountSummary[metric_key]); + } + return this.accountSummary[metric_key]; + }; + }, + }, +}; diff --git a/app/javascript/dashboard/mixins/specs/reportMixin.spec.js b/app/javascript/dashboard/mixins/specs/reportMixin.spec.js new file mode 100644 index 000000000..003295f95 --- /dev/null +++ b/app/javascript/dashboard/mixins/specs/reportMixin.spec.js @@ -0,0 +1,41 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import reportMixin from '../reportMixin'; +import reportFixtures from './reportMixinFixtures'; +import Vuex from 'vuex'; +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('reportMixin', () => { + let getters; + let store; + beforeEach(() => { + getters = { + getAccountSummary: () => reportFixtures.summary, + }; + store = new Vuex.Store({ getters }); + }); + + it('display the metric', () => { + const Component = { + render() {}, + title: 'TestComponent', + mixins: [reportMixin], + }; + const wrapper = shallowMount(Component, { store, localVue }); + expect(wrapper.vm.displayMetric('conversations_count')).toEqual(5); + expect(wrapper.vm.displayMetric('avg_first_response_time')).toEqual( + '3 Min' + ); + }); + + it('calculate the trend', () => { + const Component = { + render() {}, + title: 'TestComponent', + mixins: [reportMixin], + }; + const wrapper = shallowMount(Component, { store, localVue }); + expect(wrapper.vm.calculateTrend('conversations_count')).toEqual(25); + expect(wrapper.vm.calculateTrend('resolutions_count')).toEqual(0); + }); +}); diff --git a/app/javascript/dashboard/mixins/specs/reportMixinFixtures.js b/app/javascript/dashboard/mixins/specs/reportMixinFixtures.js new file mode 100644 index 000000000..5c8315ab1 --- /dev/null +++ b/app/javascript/dashboard/mixins/specs/reportMixinFixtures.js @@ -0,0 +1,18 @@ +export default { + summary: { + avg_first_response_time: '198.6666666666667', + avg_resolution_time: '208.3333333333333', + conversations_count: 5, + incoming_messages_count: 5, + outgoing_messages_count: 3, + previous: { + avg_first_response_time: '89.0', + avg_resolution_time: '145.0', + conversations_count: 4, + incoming_messages_count: 5, + outgoing_messages_count: 4, + resolutions_count: 0, + }, + resolutions_count: 3, + }, +}; diff --git a/app/javascript/dashboard/routes/dashboard/Dashboard.vue b/app/javascript/dashboard/routes/dashboard/Dashboard.vue index 670c93d5d..11903c9ed 100644 --- a/app/javascript/dashboard/routes/dashboard/Dashboard.vue +++ b/app/javascript/dashboard/routes/dashboard/Dashboard.vue @@ -1,9 +1,33 @@ @@ -12,16 +36,28 @@ import Sidebar from '../../components/layout/Sidebar'; import CommandBar from './commands/commandbar.vue'; import { BUS_EVENTS } from 'shared/constants/busEvents'; +import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal'; +import AddAccountModal from 'dashboard/components/layout/sidebarComponents/AddAccountModal'; +import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector'; +import AddLabelModal from 'dashboard/routes/dashboard/settings/labels/AddLabel.vue'; export default { components: { Sidebar, CommandBar, + WootKeyShortcutModal, + AddAccountModal, + AccountSelector, + AddLabelModal, }, data() { return { isSidebarOpen: false, isOnDesktop: true, + showAccountModal: false, + showCreateAccountModal: false, + showAddLabelModal: false, + showShortcutModal: false, }; }, computed: { @@ -68,6 +104,28 @@ export default { toggleSidebar() { this.isSidebarOpen = !this.isSidebarOpen; }, + openCreateAccountModal() { + this.showAccountModal = false; + this.showCreateAccountModal = true; + }, + closeCreateAccountModal() { + this.showCreateAccountModal = false; + }, + toggleAccountModal() { + this.showAccountModal = !this.showAccountModal; + }, + toggleKeyShortcutModal() { + this.showShortcutModal = true; + }, + closeKeyShortcutModal() { + this.showShortcutModal = false; + }, + showAddLabelPopup() { + this.showAddLabelModal = true; + }, + hideAddLabelPopup() { + this.showAddLabelModal = false; + }, }, }; diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue index 3f5162292..95c9ae61c 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue +++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue @@ -11,7 +11,12 @@ v-model="appliedFilters[i]" :filter-groups="filterGroups" :grouped-filters="true" - :input-type="getInputType(appliedFilters[i].attribute_key)" + :input-type=" + getInputType( + appliedFilters[i].attribute_key, + appliedFilters[i].filter_operator + ) + " :operators="getOperators(appliedFilters[i].attribute_key)" :dropdown-values="getDropdownValues(appliedFilters[i].attribute_key)" :show-query-operator="i !== appliedFilters.length - 1" @@ -86,6 +91,12 @@ export default { $each: { values: { required, + ensureBetween0to999(value, prop) { + if (prop.filter_operator === 'days_before') { + return parseInt(value, 10) > 0 && parseInt(value, 10) < 999; + } + return true; + }, }, }, }, @@ -161,7 +172,9 @@ export default { const type = this.filterTypes.find(filter => filter.attributeKey === key); return type.attributeModel; }, - getInputType(key) { + getInputType(key, operator) { + if (key === 'created_at' || key === 'last_activity_at') + if (operator === 'days_before') return 'plain_text'; const type = this.filterTypes.find(filter => filter.attributeKey === key); return type.inputType; }, diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue index 7536c0d24..dbac2f0bc 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue +++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue @@ -253,7 +253,7 @@ export default { this.$store.dispatch('contacts/get', requestParams); } else { this.$store.dispatch('contacts/search', { - search: value, + search: encodeURIComponent(value), ...requestParams, }); } diff --git a/app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js b/app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js index dc12d2640..2d54c37bc 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js +++ b/app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js @@ -1,6 +1,7 @@ import { OPERATOR_TYPES_1, OPERATOR_TYPES_3, + OPERATOR_TYPES_5, } from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js'; const filterTypes = [ { @@ -51,6 +52,30 @@ const filterTypes = [ filterOperators: OPERATOR_TYPES_3, attribute_type: 'standard', }, + { + attributeKey: 'created_at', + attributeI18nKey: 'CREATED_AT', + inputType: 'date', + dataType: 'text', + filterOperators: OPERATOR_TYPES_5, + attributeModel: 'standard', + }, + { + attributeKey: 'last_activity_at', + attributeI18nKey: 'LAST_ACTIVITY', + inputType: 'date', + dataType: 'text', + filterOperators: OPERATOR_TYPES_5, + attributeModel: 'standard', + }, + { + attributeKey: 'referer', + attributeI18nKey: 'REFERER_LINK', + inputType: 'plain_text', + dataType: 'text', + filterOperators: OPERATOR_TYPES_5, + attributeModel: 'standard', + }, ]; export const filterAttributeGroups = [ @@ -82,6 +107,14 @@ export const filterAttributeGroups = [ key: 'city', i18nKey: 'CITY', }, + { + key: 'created_at', + i18nKey: 'CREATED_AT', + }, + { + key: 'last_activity_at', + i18nKey: 'LAST_ACTIVITY', + }, ], }, ]; diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue index 90aeb43ee..24aeeff88 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue @@ -126,17 +126,15 @@ @close="toggleMergeModal" />
    - @@ -221,22 +219,11 @@ export default { return { twitter: twitterScreenName, ...(socialProfiles || {}) }; }, // Delete Modal - deleteConfirmText() { - return `${this.$t('DELETE_CONTACT.CONFIRM.YES')} ${this.contact.name}`; - }, - deleteRejectText() { - return `${this.$t('DELETE_CONTACT.CONFIRM.NO')} ${this.contact.name}`; - }, confirmDeleteMessage() { return `${this.$t('DELETE_CONTACT.CONFIRM.MESSAGE')} ${ this.contact.name } ?`; }, - confirmPlaceHolderText() { - return `${this.$t('DELETE_CONTACT.CONFIRM.PLACE_HOLDER', { - contactName: this.contact.name, - })}`; - }, }, methods: { toggleMergeModal() { diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue index 811fc95c6..b3a4ce9d7 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue @@ -34,19 +34,10 @@ {{ automation.name }} {{ automation.description }} - + /> {{ readableTime(automation.created_on) }} @@ -140,11 +131,13 @@ import AddAutomationRule from './AddAutomationRule.vue'; import EditAutomationRule from './EditAutomationRule.vue'; import alertMixin from 'shared/mixins/alertMixin'; import timeMixin from 'dashboard/mixins/time'; +import ToggleButton from 'dashboard/components/buttons/ToggleButton'; export default { components: { AddAutomationRule, EditAutomationRule, + ToggleButton, }, mixins: [alertMixin, timeMixin], data() { @@ -290,41 +283,4 @@ export default { .automation__status-checkbox { margin: 0; } -.toggle-button { - background-color: var(--s-200); - position: relative; - display: inline-flex; - height: 19px; - width: 34px; - border: 2px solid transparent; - border-radius: var(--border-radius-large); - cursor: pointer; - transition-property: background-color; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 200ms; - flex-shrink: 0; -} - -.toggle-button.active { - background-color: var(--w-500); -} - -.toggle-button span { - --space-one-point-five: 1.5rem; - height: var(--space-one-point-five); - width: var(--space-one-point-five); - display: inline-block; - background-color: var(--white); - box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px, - rgba(59, 130, 246, 0.5) 0px 0px 0px 0px, rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, - rgba(0, 0, 0, 0.06) 0px 1px 2px 0px; - transform: translate(0, 0); - border-radius: 100%; - transition-property: transform; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 200ms; -} -.toggle-button span.active { - transform: translate(var(--space-one-point-five), var(--space-zero)); -} diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/PreChatForm/Settings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/PreChatForm/Settings.vue index 6c01bc3dd..bf0c22515 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/PreChatForm/Settings.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/PreChatForm/Settings.vue @@ -15,28 +15,57 @@ - -