Compare commits

..
Author SHA1 Message Date
Vishnu Narayanan 25cdb2d265 chore: docker foss arm64 images 2024-07-05 12:46:09 +05:30
184 changed files with 3043 additions and 2972 deletions
+4 -1
View File
@@ -130,7 +130,10 @@ jobs:
command: |
mkdir -p ~/tmp/test-results/frontend_specs
~/tmp/cc-test-reporter before-build
yarn test:coverage
TESTFILES=$(circleci tests glob **/specs/*.spec.js | circleci tests split --split-by=timings)
yarn test:coverage --profile 10 \
--out ~/tmp/test-results/yarn.xml \
-- ${TESTFILES}
- run:
name: Code Climate Test Coverage
command: |
+1 -1
View File
@@ -65,10 +65,10 @@ module.exports = {
},
env: {
browser: true,
jest: true,
node: true,
},
globals: {
bus: true,
vi: true,
},
};
+7 -4
View File
@@ -24,10 +24,12 @@ jobs:
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2
with:
platforms: 'arm64,amd64'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v2
- name: Strip enterprise code
run: |
@@ -54,10 +56,11 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile
platforms: linux/amd64
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ env.DOCKER_TAG }}
+2 -2
View File
@@ -111,9 +111,9 @@ gem 'elastic-apm', require: false
gem 'newrelic_rpm', require: false
gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
gem 'scout_apm', require: false
gem 'sentry-rails', '>= 5.18.1', require: false
gem 'sentry-rails', '>= 5.18.0', require: false
gem 'sentry-ruby', require: false
gem 'sentry-sidekiq', '>= 5.18.1', require: false
gem 'sentry-sidekiq', '>= 5.18.0', require: false
##-- background job processing --##
gem 'sidekiq', '>= 7.3.0'
+65 -27
View File
@@ -148,7 +148,7 @@ GEM
barnes (0.0.9)
multi_json (~> 1)
statsd-ruby (~> 1.1)
base64 (0.1.1)
base64 (0.2.0)
bcrypt (3.1.20)
bigdecimal (3.1.8)
bindex (0.8.1)
@@ -174,9 +174,7 @@ GEM
crack (0.4.5)
rexml
crass (1.0.6)
csv (3.3.0)
csv-safe (3.3.1)
csv (~> 3.0)
csv-safe (3.2.1)
cypress-on-rails (1.16.0)
rack
database_cleaner (2.0.2)
@@ -242,9 +240,8 @@ GEM
railties (>= 5.0.0)
faker (3.2.0)
i18n (>= 1.8.11, < 2)
faraday (2.7.4)
faraday-net_http (>= 2.0, < 3.1)
ruby2_keywords (>= 0.0.4)
faraday (2.9.0)
faraday-net_http (>= 2.0, < 3.2)
faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3)
faraday-mashify (0.1.1)
@@ -252,7 +249,8 @@ GEM
hashie
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (3.0.2)
faraday-net_http (3.1.0)
net-http
faraday-net_http_persistent (2.1.0)
faraday (~> 2.5)
net-http-persistent (~> 4.0)
@@ -321,7 +319,10 @@ GEM
google-cloud-translate-v3 (0.6.0)
gapic-common (>= 0.17.1, < 2.a)
google-cloud-errors (~> 1.0)
google-protobuf (3.25.3)
google-protobuf (3.25.3-arm64-darwin)
google-protobuf (3.25.3-x86_64-darwin)
google-protobuf (3.25.3-x86_64-linux)
googleapis-common-protos (1.4.0)
google-protobuf (~> 3.14)
googleapis-common-protos-types (~> 1.2)
@@ -337,7 +338,16 @@ GEM
signet (>= 0.16, < 2.a)
groupdate (6.2.1)
activesupport (>= 5.2)
grpc (1.63.0-arm64-darwin)
grpc (1.62.0)
google-protobuf (~> 3.25)
googleapis-common-protos-types (~> 1.0)
grpc (1.62.0-arm64-darwin)
google-protobuf (~> 3.25)
googleapis-common-protos-types (~> 1.0)
grpc (1.62.0-x86_64-darwin)
google-protobuf (~> 3.25)
googleapis-common-protos-types (~> 1.0)
grpc (1.62.0-x86_64-linux)
google-protobuf (~> 3.25)
googleapis-common-protos-types (~> 1.0)
haikunator (1.1.1)
@@ -361,7 +371,7 @@ GEM
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
httpclient (2.8.3)
i18n (1.14.1)
i18n (1.14.5)
concurrent-ruby (~> 1.0)
image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5)
@@ -385,7 +395,8 @@ GEM
hana (~> 1.3)
regexp_parser (~> 2.0)
uri_template (~> 0.7)
jwt (2.7.0)
jwt (2.8.1)
base64
kaminari (1.2.2)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2)
@@ -409,8 +420,15 @@ GEM
letter_opener (1.8.1)
launchy (>= 2.2, < 3)
libdatadog (7.0.0.1.0)
libdatadog (7.0.0.1.0-x86_64-linux)
libddwaf (1.14.0.0.0)
ffi (~> 1.0)
libddwaf (1.14.0.0.0-arm64-darwin)
ffi (~> 1.0)
libddwaf (1.14.0.0.0-x86_64-darwin)
ffi (~> 1.0)
libddwaf (1.14.0.0.0-x86_64-linux)
ffi (~> 1.0)
line-bot-api (1.28.0)
liquid (5.4.0)
listen (3.8.0)
@@ -439,13 +457,14 @@ GEM
meta_request (0.8.2)
rack-contrib (>= 1.1, < 3)
railties (>= 3.0.0, < 8)
method_source (1.0.0)
method_source (1.1.0)
mime-types (3.4.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2023.0218.1)
mini_magick (4.12.0)
mini_mime (1.1.5)
minitest (5.24.1)
mini_portile2 (2.8.7)
minitest (5.24.0)
mock_redis (0.36.0)
ruby2_keywords
msgpack (1.7.0)
@@ -454,6 +473,8 @@ GEM
multipart-post (2.3.0)
neighbor (0.2.3)
activerecord (>= 5.2)
net-http (0.4.1)
uri
net-http-persistent (4.0.2)
connection_pool (~> 2.2)
net-imap (0.4.12)
@@ -472,8 +493,15 @@ GEM
newrelic_rpm (9.6.0)
base64
nio4r (2.7.3)
nokogiri (1.16.6)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.16.6-arm64-darwin)
racc (~> 1.4)
nokogiri (1.16.6-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.16.6-x86_64-linux)
racc (~> 1.4)
oauth (1.1.0)
oauth-tty (~> 1.0, >= 1.0.1)
snaky_hash (~> 2.0)
@@ -536,7 +564,8 @@ GEM
rack (>= 2.0.0)
rack-mini-profiler (3.2.0)
rack (>= 1.2.0)
rack-protection (3.1.0)
rack-protection (3.2.0)
base64 (>= 0.1.0)
rack (~> 2.2, >= 2.2.4)
rack-proxy (0.7.6)
rack
@@ -572,7 +601,7 @@ GEM
thor (~> 1.0)
zeitwerk (~> 2.5)
rainbow (3.1.1)
rake (13.1.0)
rake (13.2.1)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
@@ -602,8 +631,8 @@ GEM
retriable (3.1.2)
reverse_markdown (2.1.1)
nokogiri
rexml (3.3.1)
strscan
rexml (3.2.8)
strscan (>= 3.0.9)
rspec-core (3.13.0)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.1)
@@ -678,14 +707,14 @@ GEM
activesupport (>= 4)
selectize-rails (0.12.6)
semantic_range (3.0.0)
sentry-rails (5.18.1)
sentry-rails (5.18.0)
railties (>= 5.0)
sentry-ruby (~> 5.18.1)
sentry-ruby (5.18.1)
sentry-ruby (~> 5.18.0)
sentry-ruby (5.18.0)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
sentry-sidekiq (5.18.1)
sentry-ruby (~> 5.18.1)
sentry-sidekiq (5.18.0)
sentry-ruby (~> 5.18.0)
sidekiq (>= 3.0)
sexp_processor (4.17.0)
shoulda-matchers (5.3.0)
@@ -737,7 +766,7 @@ GEM
strscan (3.1.0)
telephone_number (1.4.20)
test-prof (1.2.1)
thor (1.3.0)
thor (1.3.1)
tilt (2.3.0)
time_diff (0.3.0)
activesupport
@@ -762,11 +791,12 @@ GEM
unf_ext (0.0.8.2)
unicode-display_width (2.4.2)
uniform_notifier (1.16.0)
uri (0.13.0)
uri_template (0.7.0)
valid_email2 (4.0.6)
activemodel (>= 3.2)
mail (~> 2.5)
version_gem (1.1.3)
version_gem (1.1.4)
warden (1.2.9)
rack (>= 2.0.9)
web-console (4.2.1)
@@ -797,7 +827,15 @@ GEM
zeitwerk (2.6.16)
PLATFORMS
arm64-darwin-23
arm64-darwin-20
arm64-darwin-21
arm64-darwin-22
ruby
x86_64-darwin-18
x86_64-darwin-20
x86_64-darwin-21
x86_64-darwin-22
x86_64-linux
DEPENDENCIES
active_record_query_trace
@@ -898,9 +936,9 @@ DEPENDENCIES
scout_apm
scss_lint
seed_dump
sentry-rails (>= 5.18.1)
sentry-rails (>= 5.18.0)
sentry-ruby
sentry-sidekiq (>= 5.18.1)
sentry-sidekiq (>= 5.18.0)
shoulda-matchers
sidekiq (>= 7.3.0)
sidekiq-cron (>= 1.12.0)
@@ -1,30 +0,0 @@
class V2::BaseConversationReportBuilder
pattr_initialize :account, :params
private
AVG_METRICS = %w[avg_first_response_time avg_resolution_time reply_time].freeze
COUNT_METRICS = %w[
conversations_count
incoming_messages_count
outgoing_messages_count
resolutions_count
bot_resolutions_count
bot_handoffs_count
].freeze
def builder_class(metric)
case metric
when *AVG_METRICS
V2::Reports::Timeseries::AverageReportBuilder
when *COUNT_METRICS
V2::Reports::Timeseries::CountReportBuilder
end
end
def log_invalid_metric
Rails.logger.error "ReportBuilder: Invalid metric - #{params[:metric]}"
{}
end
end
@@ -1,30 +0,0 @@
class V2::ConversationMetricBuilder < V2::BaseConversationReportBuilder
def summary
{
conversations_count: count('conversations_count'),
incoming_messages_count: count('incoming_messages_count'),
outgoing_messages_count: count('outgoing_messages_count'),
avg_first_response_time: count('avg_first_response_time'),
avg_resolution_time: count('avg_resolution_time'),
resolutions_count: count('resolutions_count'),
reply_time: count('reply_time')
}
end
def bot_summary
{
bot_resolutions_count: count('bot_resolutions_count'),
bot_handoffs_count: count('bot_handoffs_count')
}
end
private
def count(metric)
builder_class(metric).new(account, builder_params(metric)).aggregate_value
end
def builder_params(metric)
params.merge({ metric: metric })
end
end
@@ -1,25 +0,0 @@
class V2::ConversationReportBuilder < V2::BaseConversationReportBuilder
include DateRangeHelper
pattr_initialize :account, :params
def timeseries
perform_action(:timeseries)
end
def aggregate_value
perform_action(:aggregate_value)
end
private
def perform_action(method_name)
return builder.new(account, params).public_send(method_name) if builder.present?
log_invalid_metric
end
def builder
builder_class(params[:metric])
end
end
@@ -3,7 +3,6 @@ class V2::Reports::AgentSummaryBuilder < V2::Reports::BaseSummaryBuilder
def build
set_grouped_conversations_count
set_grouped_resolved_conversations_count
set_grouped_avg_reply_time
set_grouped_avg_first_response_time
set_grouped_avg_resolution_time
@@ -28,15 +27,6 @@ class V2::Reports::AgentSummaryBuilder < V2::Reports::BaseSummaryBuilder
@grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time'))
end
def set_grouped_resolved_conversations_count
@grouped_resolved_conversations_count = reporting_events
.where(name: 'conversation_resolved')
.select(group_by_key, :conversation_id)
.distinct
.group(group_by_key)
.count(:conversation_id)
end
def group_by_key
:user_id
end
@@ -47,14 +37,12 @@ class V2::Reports::AgentSummaryBuilder < V2::Reports::BaseSummaryBuilder
def prepare_report
account.account_users.each_with_object([]) do |account_user, arr|
user_id = account_user.user_id
arr << {
id: user_id,
conversations_count: @grouped_conversations_count[user_id],
resolved_conversations_count: @grouped_resolved_conversations_count[user_id],
avg_resolution_time: @grouped_avg_resolution_time[user_id],
avg_first_response_time: @grouped_avg_first_response_time[user_id],
avg_reply_time: @grouped_avg_reply_time[user_id]
id: account_user.user_id,
conversations_count: @grouped_conversations_count[account_user.user_id],
avg_resolution_time: @grouped_avg_resolution_time[account_user.user_id],
avg_first_response_time: @grouped_avg_first_response_time[account_user.user_id],
avg_reply_time: @grouped_avg_reply_time[account_user.user_id]
}
end
end
@@ -1,65 +0,0 @@
class V2::Reports::InboxSummaryBuilder < V2::Reports::BaseSummaryBuilder
pattr_initialize [:account!, :params!]
def build
set_grouped_conversations_count
set_grouped_resolved_conversations_count
set_grouped_avg_reply_time
set_grouped_avg_first_response_time
set_grouped_avg_resolution_time
prepare_report
end
private
def set_grouped_conversations_count
@grouped_conversations_count = Current.account.conversations.where(created_at: range).group('inbox_id').count
end
def set_grouped_avg_resolution_time
@grouped_avg_resolution_time = get_grouped_average(reporting_events.where(name: 'conversation_resolved'))
end
def set_grouped_avg_first_response_time
@grouped_avg_first_response_time = get_grouped_average(reporting_events.where(name: 'first_response'))
end
def set_grouped_avg_reply_time
@grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time'))
end
def set_grouped_resolved_conversations_count
@grouped_resolved_conversations_count = reporting_events
.where(name: 'conversation_resolved')
.select(group_by_key, :conversation_id)
.distinct
.group(group_by_key)
.count(:conversation_id)
end
def group_by_key
:inbox_id
end
def reporting_events
@reporting_events ||= Current.account.reporting_events.where(created_at: range)
end
def prepare_report
account.inboxes.each_with_object([]) do |inbox, arr|
inbox_id = inbox.id
arr << {
id: inbox_id,
conversations_count: @grouped_conversations_count[inbox_id],
resolved_conversations_count: @grouped_resolved_conversations_count[inbox_id],
avg_resolution_time: @grouped_avg_resolution_time[inbox_id],
avg_first_response_time: @grouped_avg_first_response_time[inbox_id],
avg_reply_time: @grouped_avg_reply_time[inbox_id]
}
end
end
def average_value_key
ActiveModel::Type::Boolean.new.cast(params[:business_hours]).present? ? :value_in_business_hours : :value
end
end
@@ -3,7 +3,6 @@ class V2::Reports::TeamSummaryBuilder < V2::Reports::BaseSummaryBuilder
def build
set_grouped_conversations_count
set_grouped_resolved_conversations_count
set_grouped_avg_reply_time
set_grouped_avg_first_response_time
set_grouped_avg_resolution_time
@@ -28,15 +27,6 @@ class V2::Reports::TeamSummaryBuilder < V2::Reports::BaseSummaryBuilder
@grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time'))
end
def set_grouped_resolved_conversations_count
@grouped_resolved_conversations_count = reporting_events
.where(name: 'conversation_resolved')
.select(group_by_key, :conversation_id)
.distinct
.group(group_by_key)
.count(:conversation_id)
end
def reporting_events
@reporting_events ||= Current.account.reporting_events.where(created_at: range).joins(:conversation)
end
@@ -50,7 +40,6 @@ class V2::Reports::TeamSummaryBuilder < V2::Reports::BaseSummaryBuilder
arr << {
id: team.id,
conversations_count: @grouped_conversations_count[team.id],
resolved_conversations_count: @grouped_resolved_conversations_count[team.id],
avg_resolution_time: @grouped_avg_resolution_time[team.id],
avg_first_response_time: @grouped_avg_first_response_time[team.id],
avg_reply_time: @grouped_avg_reply_time[team.id]
@@ -1,57 +0,0 @@
class Api::V2::Accounts::LiveReportsController < Api::V1::Accounts::BaseController
before_action :load_conversations, only: [:conversation_metrics, :grouped_conversation_metrics]
before_action :set_group_scope, only: [:grouped_conversation_metrics]
def conversation_metrics
render json: {
open: @conversations.open.count,
unattended: @conversations.open.unattended.count,
unassigned: @conversations.open.unassigned.count
}
end
def grouped_conversation_metrics
count_by_group = @conversations.open.group(@group_scope).count
unattended_by_group = @conversations.open.unattended.group(@group_scope).count
unassigned_by_group = @conversations.open.unassigned.group(@group_scope).count
group_metrics = count_by_group.map do |group_id, count|
metric = {
open: count,
unattended: unattended_by_group[group_id] || 0,
unassigned: unassigned_by_group[group_id] || 0
}
metric[@group_scope] = group_id
metric
end
render json: group_metrics
end
private
def set_group_scope
render json: { error: 'invalid group_by' }, status: :unprocessable_entity and return unless %w[
team_id
assignee_id
].include?(permitted_params[:group_by])
@group_scope = permitted_params[:group_by]
end
def team
return unless permitted_params[:team_id]
@team ||= Current.account.teams.find(permitted_params[:team_id])
end
def load_conversations
scope = Current.account.conversations
scope = scope.where(team_id: team.id) if team.present?
@conversations = scope
end
def permitted_params
params.permit(:team_id, :group_by)
end
end
@@ -1,6 +1,6 @@
class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseController
before_action :check_authorization
before_action :prepare_builder_params, only: [:agent, :team, :inbox]
before_action :prepare_builder_params, only: [:agent, :team]
def agent
render_report_with(V2::Reports::AgentSummaryBuilder)
@@ -10,10 +10,6 @@ class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseContr
render_report_with(V2::Reports::TeamSummaryBuilder)
end
def inbox
render_report_with(V2::Reports::InboxSummaryBuilder)
end
private
def check_authorization
@@ -94,8 +94,7 @@ module Api::V2::Accounts::HeatmapHelper
end
def since_timestamp(date)
number_of_days = params[:days_before].present? ? params[:days_before].to_i.days : 6.days
(date - number_of_days).to_i.to_s
(date - 6.days).to_i.to_s
end
def until_timestamp(date)
+11
View File
@@ -0,0 +1,11 @@
import App from './App';
import '../../test-matchers';
describe(`App component`, () => {
it(`should be a component`, () => {
// Arrange
// Act
expect(App).toBeVueComponent('App');
// Assert
});
});
@@ -15,10 +15,10 @@ describe('#enterpriseAccountAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -1,20 +0,0 @@
/* global axios */
import ApiClient from './ApiClient';
class LiveReportsAPI extends ApiClient {
constructor() {
super('live_reports', { accountScoped: true, apiVersion: 'v2' });
}
getConversationMetric(params = {}) {
return axios.get(`${this.url}/conversation_metrics`, { params });
}
getGroupedConversations({ groupBy } = { groupBy: 'assignee_id' }) {
return axios.get(`${this.url}/grouped_conversation_metrics`, {
params: { group_by: groupBy },
});
}
}
export default new LiveReportsAPI();
+2 -2
View File
@@ -61,9 +61,9 @@ class ReportsAPI extends ApiClient {
});
}
getConversationTrafficCSV({ daysBefore = 6 } = {}) {
getConversationTrafficCSV() {
return axios.get(`${this.url}/conversation_traffic`, {
params: { timezone_offset: getTimeOffset(), days_before: daysBefore },
params: { timezone_offset: getTimeOffset() },
});
}
@@ -15,10 +15,10 @@ describe('#accountAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -10,10 +10,10 @@ describe('#ContactsAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -14,7 +14,7 @@ describe('#AgentAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -14,10 +14,10 @@ describe('#PortalAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -44,10 +44,10 @@ describe('#PortalAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -71,10 +71,10 @@ describe('#PortalAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -98,10 +98,10 @@ describe('#PortalAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -129,10 +129,10 @@ describe('#PortalAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -4,10 +4,10 @@ describe('#AssignableAgentsAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -13,10 +13,10 @@ describe('#FBChannel', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -17,10 +17,10 @@ describe('#ContactsAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -16,10 +16,10 @@ describe('#ConversationApi', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -11,10 +11,10 @@ describe('#Reports API', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -24,10 +24,10 @@ describe('#ConversationAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -15,10 +15,10 @@ describe('#ConversationAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -17,10 +17,10 @@ describe('#InboxesAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -18,10 +18,10 @@ describe('#integrationAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -11,10 +11,10 @@ describe('#accountAPI', () => {
describe('createAMeeting', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -39,10 +39,10 @@ describe('#accountAPI', () => {
describe('addParticipantToMeeting', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -16,10 +16,10 @@ describe('#linearAPI', () => {
describe('getTeams', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -41,10 +41,10 @@ describe('#linearAPI', () => {
describe('getTeamEntities', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -66,10 +66,10 @@ describe('#linearAPI', () => {
describe('createIssue', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -96,10 +96,10 @@ describe('#linearAPI', () => {
describe('link_issue', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -125,10 +125,10 @@ describe('#linearAPI', () => {
describe('getLinkedIssue', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -150,10 +150,10 @@ describe('#linearAPI', () => {
describe('unlinkIssue', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -178,10 +178,10 @@ describe('#linearAPI', () => {
describe('searchIssues', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -13,10 +13,10 @@ describe('#NotificationAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -20,10 +20,10 @@ describe('#Reports API', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -12,10 +12,10 @@ describe('#SLAReports API', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -16,10 +16,10 @@ describe('#TeamsAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -1,40 +0,0 @@
/* global axios */
import ApiClient from './ApiClient';
class SummaryReportsAPI extends ApiClient {
constructor() {
super('summary_reports', { accountScoped: true, apiVersion: 'v2' });
}
getTeamReports({ since, until, businessHours } = {}) {
return axios.get(`${this.url}/team`, {
params: {
since,
until,
business_hours: businessHours,
},
});
}
getAgentReports({ since, until, businessHours } = {}) {
return axios.get(`${this.url}/agent`, {
params: {
since,
until,
business_hours: businessHours,
},
});
}
getInboxReports({ since, until, businessHours } = {}) {
return axios.get(`${this.url}/inbox`, {
params: {
since,
until,
business_hours: businessHours,
},
});
}
}
export default new SummaryReportsAPI();
@@ -1,12 +1,12 @@
import AccountSelector from '../AccountSelector.vue';
import AccountSelector from '../AccountSelector';
import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import i18n from 'dashboard/i18n';
import WootModal from 'dashboard/components/Modal.vue';
import WootModalHeader from 'dashboard/components/ModalHeader.vue';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
import WootModal from 'dashboard/components/Modal';
import WootModalHeader from 'dashboard/components/ModalHeader';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
const localVue = createLocalVue();
localVue.component('woot-modal', WootModal);
@@ -1,12 +1,12 @@
import AgentDetails from '../AgentDetails.vue';
import AgentDetails from '../AgentDetails';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import VTooltip from 'v-tooltip';
import i18n from 'dashboard/i18n';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import WootButton from 'dashboard/components/ui/WootButton.vue';
import Thumbnail from 'dashboard/components/widgets/Thumbnail';
import WootButton from 'dashboard/components/ui/WootButton';
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.use(VueI18n);
@@ -1,15 +1,13 @@
import NotificationBell from '../NotificationBell.vue';
import NotificationBell from '../NotificationBell';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
import i18n from 'dashboard/i18n';
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.use(VueI18n);
localVue.component('fluent-icon', FluentIcon);
const i18nConfig = new VueI18n({
locale: 'en',
@@ -29,7 +27,7 @@ describe('notificationBell', () => {
beforeEach(() => {
actions = {
showNotification: vi.fn(),
showNotification: jest.fn(),
};
modules = {
auth: {
@@ -2,21 +2,15 @@ import AvailabilityStatus from '../AvailabilityStatus.vue';
import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import VTooltip from 'v-tooltip';
import WootButton from 'dashboard/components/ui/WootButton.vue';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue';
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider.vue';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
import WootButton from 'dashboard/components/ui/WootButton';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader';
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider';
import i18n from 'dashboard/i18n';
const localVue = createLocalVue();
localVue.use(VTooltip, {
defaultHtml: false,
});
localVue.use(Vuex);
localVue.use(VueI18n);
localVue.component('woot-button', WootButton);
@@ -24,14 +18,12 @@ localVue.component('woot-dropdown-header', WootDropdownHeader);
localVue.component('woot-dropdown-menu', WootDropdownMenu);
localVue.component('woot-dropdown-divider', WootDropdownDivider);
localVue.component('woot-dropdown-item', WootDropdownItem);
localVue.component('fluent-icon', FluentIcon);
const i18nConfig = new VueI18n({ locale: 'en', messages: i18n });
describe('AvailabilityStatus', () => {
const currentAvailability = 'online';
const currentAccountId = '1';
const currentUserAutoOffline = false;
let store = null;
let actions = null;
let modules = null;
@@ -39,7 +31,7 @@ describe('AvailabilityStatus', () => {
beforeEach(() => {
actions = {
updateAvailability: vi.fn(() => {
updateAvailability: jest.fn(() => {
return Promise.resolve();
}),
};
@@ -49,7 +41,6 @@ describe('AvailabilityStatus', () => {
getters: {
getCurrentUserAvailability: () => currentAvailability,
getCurrentAccountId: () => currentAccountId,
getCurrentUserAutoOffline: () => currentUserAutoOffline,
},
},
};
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import SidemenuIcon from '../SidemenuIcon.vue';
import SidemenuIcon from '../SidemenuIcon';
describe('SidemenuIcon', () => {
test('matches snapshot', () => {
@@ -1,6 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SidemenuIcon > matches snapshot 1`] = `
exports[`SidemenuIcon matches snapshot 1`] = `
<button
class="-ml-3 text-black-900 dark:text-slate-300"
color-scheme="secondary"
@@ -18,11 +18,11 @@ describe('Date formatting functions', () => {
const testDate = new Date(2020, 4, 15); // May 15, 2020
beforeEach(() => {
vi.spyOn(navigator, 'language', 'get').mockReturnValue('en-US');
jest.spyOn(navigator, 'language', 'get').mockReturnValue('en-US');
});
afterEach(() => {
vi.restoreAllMocks();
jest.restoreAllMocks();
});
it('returns the correct month name from a date', () => {
@@ -39,7 +39,7 @@ describe('Date formatting functions', () => {
});
it('returns the correct date format for the current locale en-IN', () => {
vi.spyOn(navigator, 'language', 'get').mockReturnValue('en-IN');
jest.spyOn(navigator, 'language', 'get').mockReturnValue('en-IN');
const expected = 'dd/MM/yyyy';
expect(getIntlDateFormatForLocale()).toBe(expected);
});
@@ -231,13 +231,13 @@ describe('isHoveringNextDayInRange', () => {
describe('getActiveDateRange', () => {
const currentDate = new Date(2020, 5, 15, 12, 0); // May 15, 2020, at noon
beforeEach(() => {
beforeAll(() => {
// Mocking the current date to ensure consistency in tests
vi.useFakeTimers().setSystemTime(currentDate.getTime());
jest.useFakeTimers().setSystemTime(currentDate.getTime());
});
afterEach(() => {
vi.useRealTimers();
afterAll(() => {
jest.useRealTimers();
});
it('returns the correct range for "last7days"', () => {
@@ -197,7 +197,7 @@ import {
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage';
const EmojiInput = () => import('shared/components/emoji/EmojiInput.vue');
const EmojiInput = () => import('shared/components/emoji/EmojiInput');
export default {
components: {
@@ -2,16 +2,16 @@ import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import VTooltip from 'v-tooltip';
import Button from 'dashboard/components/buttons/Button.vue';
import Button from 'dashboard/components/buttons/Button';
import i18n from 'dashboard/i18n';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
import MoreActions from '../MoreActions.vue';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
import MoreActions from '../MoreActions';
vi.mock('shared/helpers/mitt', () => ({
jest.mock('shared/helpers/mitt', () => ({
emitter: {
emit: vi.fn(),
on: vi.fn(),
off: vi.fn(),
emit: jest.fn(),
on: jest.fn(),
off: jest.fn(),
},
}));
@@ -26,9 +26,9 @@ localVue.component('fluent-icon', FluentIcon);
localVue.component('woot-button', Button);
localVue.prototype.$emitter = {
emit: vi.fn(),
on: vi.fn(),
off: vi.fn(),
emit: jest.fn(),
on: jest.fn(),
off: jest.fn(),
};
const i18nConfig = new VueI18n({ locale: 'en', messages: i18n });
@@ -49,8 +49,8 @@ describe('MoveActions', () => {
currentChat,
};
muteConversation = vi.fn(() => Promise.resolve());
unmuteConversation = vi.fn(() => Promise.resolve());
muteConversation = jest.fn(() => Promise.resolve());
unmuteConversation = jest.fn(() => Promise.resolve());
modules = {
conversations: { actions: { muteConversation, unmuteConversation } },
@@ -8,7 +8,7 @@ const buildComponent = ({ data = {}, methods = {} }) => ({
data() {
return { ...data, selectedIndex: 0, items: [1, 2, 3] };
},
methods: { ...methods, onSelect: vi.fn(), adjustScroll: vi.fn() },
methods: { ...methods, onSelect: jest.fn(), adjustScroll: jest.fn() },
mixins: [keyboardEventListenerMixins],
});
@@ -21,7 +21,7 @@ describe('mentionSelectionKeyboardMixin', () => {
});
it('ArrowUp and Control+KeyP update selectedIndex correctly', () => {
const preventDefault = vi.fn();
const preventDefault = jest.fn();
const keyboardEvents = wrapper.vm.getKeyboardEvents();
if (keyboardEvents && keyboardEvents.ArrowUp) {
@@ -39,7 +39,7 @@ describe('mentionSelectionKeyboardMixin', () => {
});
it('ArrowDown and Control+KeyN update selectedIndex correctly', () => {
const preventDefault = vi.fn();
const preventDefault = jest.fn();
const keyboardEvents = wrapper.vm.getKeyboardEvents();
if (keyboardEvents && keyboardEvents.ArrowDown) {
@@ -57,7 +57,7 @@ describe('mentionSelectionKeyboardMixin', () => {
});
it('Enter key triggers onSelect method', () => {
const preventDefault = vi.fn();
const preventDefault = jest.fn();
const keyboardEvents = wrapper.vm.getKeyboardEvents();
if (keyboardEvents && keyboardEvents.Enter) {
@@ -2,17 +2,17 @@ import { shallowMount } from '@vue/test-utils';
import { emitter } from 'shared/helpers/mitt';
import { useEmitter } from '../emitter';
vi.mock('shared/helpers/mitt', () => ({
jest.mock('shared/helpers/mitt', () => ({
emitter: {
on: vi.fn(),
off: vi.fn(),
on: jest.fn(),
off: jest.fn(),
},
}));
describe('useEmitter', () => {
let wrapper;
const eventName = 'my-event';
const callback = vi.fn();
const callback = jest.fn();
beforeEach(() => {
wrapper = shallowMount({
@@ -29,6 +29,10 @@ describe('useEmitter', () => {
});
});
afterEach(() => {
jest.resetAllMocks();
});
it('should add an event listener on mount', () => {
expect(emitter.on).toHaveBeenCalledWith(eventName, callback);
});
@@ -1,13 +1,13 @@
import helperObject, { AnalyticsHelper } from '../';
vi.mock('@june-so/analytics-next', () => ({
jest.mock('@june-so/analytics-next', () => ({
AnalyticsBrowser: {
load: () => [
{
identify: vi.fn(),
track: vi.fn(),
page: vi.fn(),
group: vi.fn(),
identify: jest.fn(),
track: jest.fn(),
page: jest.fn(),
group: jest.fn(),
},
],
},
@@ -40,7 +40,7 @@ describe('AnalyticsHelper', () => {
describe('identify', () => {
beforeEach(() => {
analyticsHelper.analytics = { identify: vi.fn(), group: vi.fn() };
analyticsHelper.analytics = { identify: jest.fn(), group: jest.fn() };
});
it('should call identify on analytics browser with correct arguments', () => {
@@ -87,7 +87,7 @@ describe('AnalyticsHelper', () => {
describe('track', () => {
beforeEach(() => {
analyticsHelper.analytics = { track: vi.fn() };
analyticsHelper.analytics = { track: jest.fn() };
analyticsHelper.user = { id: '123' };
});
@@ -118,7 +118,7 @@ describe('AnalyticsHelper', () => {
describe('page', () => {
beforeEach(() => {
analyticsHelper.analytics = { page: vi.fn() };
analyticsHelper.analytics = { page: jest.fn() };
});
it('should call the analytics.page method with the correct arguments', () => {
@@ -2,35 +2,34 @@ import Vue from 'vue';
import plugin from '../plugin';
import analyticsHelper from '../index';
vi.spyOn(analyticsHelper, 'init');
vi.spyOn(analyticsHelper, 'track');
describe('Vue Analytics Plugin', () => {
beforeEach(() => {
jest.spyOn(analyticsHelper, 'init');
jest.spyOn(analyticsHelper, 'track');
Vue.use(plugin);
});
it('should call the init method on analyticsHelper once during plugin installation', () => {
expect(analyticsHelper.init).toHaveBeenCalledTimes(1);
afterEach(() => {
jest.resetModules();
jest.clearAllMocks();
});
it('should add the analyticsHelper to the Vue prototype as $analytics', () => {
it('should call the init method on the analyticsHelper', () => {
expect(analyticsHelper.init).toHaveBeenCalled();
});
it('should add the analyticsHelper to the Vue prototype', () => {
expect(Vue.prototype.$analytics).toBe(analyticsHelper);
});
it('should add a track method to the Vue prototype as $track', () => {
it('should add the track method to the Vue prototype', () => {
expect(typeof Vue.prototype.$track).toBe('function');
Vue.prototype.$track('eventName');
expect(analyticsHelper.track)
.toHaveBeenCalledTimes(1)
.toHaveBeenCalledWith('eventName');
expect(analyticsHelper.track).toHaveBeenCalledWith('eventName');
});
it('should call the track method on analyticsHelper with the correct event name when $track is called', () => {
const eventName = 'testEvent';
Vue.prototype.$track(eventName);
expect(analyticsHelper.track)
.toHaveBeenCalledTimes(1)
.toHaveBeenCalledWith(eventName);
it('should call the track method on the analyticsHelper when $track is called', () => {
Vue.prototype.$track('eventName');
expect(analyticsHelper.track).toHaveBeenCalledWith('eventName');
});
});
@@ -9,11 +9,6 @@ import {
const MAX_DISCONNECT_SECONDS = 10800;
// The disconnect delay threshold is added to account for delays in identifying
// disconnections (for example, the websocket disconnection takes up to 3 seconds)
// while fetching the latest updated conversations or messages.
const DISCONNECT_DELAY_THRESHOLD = 15;
class ReconnectService {
constructor(store, router) {
this.store = store;
@@ -52,8 +47,7 @@ class ReconnectService {
fetchConversations = async () => {
await this.store.dispatch('updateChatListFilters', {
page: null,
updatedWithin:
this.getSecondsSinceDisconnect() + DISCONNECT_DELAY_THRESHOLD,
updatedWithin: this.getSecondsSinceDisconnect(),
});
await this.store.dispatch('fetchAllConversations');
// Reset the updatedWithin in the store chat list filter after fetching conversations when the user is reconnected
@@ -73,14 +73,7 @@ class ActionCableConnector extends BaseActionCableConnector {
const { id } = payload;
if (id) {
this.app.$store.dispatch('updateConversation', payload);
} else {
// eslint-disable-next-line no-console
console.warn(
'[Chatwoot] Skipping assignee change event with payload: ',
payload
);
}
this.fetchConversationStats();
};
@@ -4,7 +4,7 @@ describe('DataManager', () => {
const accountId = 'test-account';
let dataManager;
beforeEach(async () => {
beforeAll(async () => {
dataManager = new DataManager(accountId);
await dataManager.initDb();
});
@@ -8,26 +8,26 @@ import {
} from 'dashboard/helper/routeHelpers';
import ReconnectService from 'dashboard/helper/ReconnectService';
vi.mock('shared/helpers/mitt', () => ({
jest.mock('shared/helpers/mitt', () => ({
emitter: {
on: vi.fn(),
off: vi.fn(),
emit: vi.fn(),
on: jest.fn(),
off: jest.fn(),
emit: jest.fn(),
},
}));
vi.mock('date-fns', () => ({
differenceInSeconds: vi.fn(),
jest.mock('date-fns', () => ({
differenceInSeconds: jest.fn(),
}));
vi.mock('dashboard/helper/routeHelpers', () => ({
isAConversationRoute: vi.fn(),
isAInboxViewRoute: vi.fn(),
isNotificationRoute: vi.fn(),
jest.mock('dashboard/helper/routeHelpers', () => ({
isAConversationRoute: jest.fn(),
isAInboxViewRoute: jest.fn(),
isNotificationRoute: jest.fn(),
}));
const storeMock = {
dispatch: vi.fn(),
dispatch: jest.fn(),
getters: {
getAppliedConversationFiltersQuery: [],
'customViews/getActiveConversationFolder': { query: {} },
@@ -46,17 +46,17 @@ describe('ReconnectService', () => {
let reconnectService;
beforeEach(() => {
window.addEventListener = vi.fn();
window.removeEventListener = vi.fn();
window.addEventListener = jest.fn();
window.removeEventListener = jest.fn();
Object.defineProperty(window, 'location', {
configurable: true,
value: { reload: vi.fn() },
value: { reload: jest.fn() },
});
reconnectService = new ReconnectService(storeMock, routerMock);
});
afterEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
});
describe('constructor', () => {
@@ -102,7 +102,7 @@ describe('ReconnectService', () => {
expect(reconnectService.getSecondsSinceDisconnect()).toBe(0);
});
it('should return the number of seconds + threshold since disconnect', () => {
it('should return the number of seconds since disconnect', () => {
reconnectService.disconnectTime = new Date();
differenceInSeconds.mockReturnValue(100);
expect(reconnectService.getSecondsSinceDisconnect()).toBe(100);
@@ -111,7 +111,7 @@ describe('ReconnectService', () => {
describe('handleOnlineEvent', () => {
it('should reload the page if disconnected for more than 3 hours', () => {
reconnectService.getSecondsSinceDisconnect = vi
reconnectService.getSecondsSinceDisconnect = jest
.fn()
.mockReturnValue(10801);
reconnectService.handleOnlineEvent();
@@ -119,7 +119,7 @@ describe('ReconnectService', () => {
});
it('should not reload the page if disconnected for less than 3 hours', () => {
reconnectService.getSecondsSinceDisconnect = vi
reconnectService.getSecondsSinceDisconnect = jest
.fn()
.mockReturnValue(10799);
reconnectService.handleOnlineEvent();
@@ -128,27 +128,22 @@ describe('ReconnectService', () => {
});
describe('fetchConversations', () => {
it('should update the filters with disconnected time and the threshold', async () => {
reconnectService.getSecondsSinceDisconnect = vi.fn().mockReturnValue(100);
await reconnectService.fetchConversations();
expect(storeMock.dispatch).toHaveBeenCalledWith('updateChatListFilters', {
page: null,
updatedWithin: 115,
});
});
it('should dispatch updateChatListFilters and fetchAllConversations', async () => {
reconnectService.getSecondsSinceDisconnect = vi.fn().mockReturnValue(100);
reconnectService.getSecondsSinceDisconnect = jest
.fn()
.mockReturnValue(100);
await reconnectService.fetchConversations();
expect(storeMock.dispatch).toHaveBeenCalledWith('updateChatListFilters', {
page: null,
updatedWithin: 115,
updatedWithin: 100,
});
expect(storeMock.dispatch).toHaveBeenCalledWith('fetchAllConversations');
});
it('should dispatch updateChatListFilters and reset updatedWithin', async () => {
reconnectService.getSecondsSinceDisconnect = vi.fn().mockReturnValue(100);
reconnectService.getSecondsSinceDisconnect = jest
.fn()
.mockReturnValue(100);
await reconnectService.fetchConversations();
expect(storeMock.dispatch).toHaveBeenCalledWith('updateChatListFilters', {
updatedWithin: null,
@@ -178,7 +173,7 @@ describe('ReconnectService', () => {
},
],
};
const spy = vi.spyOn(
const spy = jest.spyOn(
reconnectService,
'fetchFilteredOrSavedConversations'
);
@@ -196,7 +191,7 @@ describe('ReconnectService', () => {
query: null,
};
const spy = vi.spyOn(reconnectService, 'fetchConversations');
const spy = jest.spyOn(reconnectService, 'fetchConversations');
await reconnectService.fetchConversationsOnReconnect();
@@ -209,7 +204,7 @@ describe('ReconnectService', () => {
query: { test: 'activeFolderQuery' },
};
const spy = vi.spyOn(
const spy = jest.spyOn(
reconnectService,
'fetchFilteredOrSavedConversations'
);
@@ -275,11 +270,11 @@ describe('ReconnectService', () => {
describe('handleRouteSpecificFetch', () => {
it('should fetch conversations and messages if current route is a conversation route', async () => {
isAConversationRoute.mockReturnValue(true);
const spyConversations = vi.spyOn(
const spyConversations = jest.spyOn(
reconnectService,
'fetchConversationsOnReconnect'
);
const spyMessages = vi.spyOn(
const spyMessages = jest.spyOn(
reconnectService,
'fetchConversationMessagesOnReconnect'
);
@@ -290,14 +285,14 @@ describe('ReconnectService', () => {
it('should fetch notifications if current route is an inbox view route', async () => {
isAInboxViewRoute.mockReturnValue(true);
const spy = vi.spyOn(reconnectService, 'fetchNotificationsOnReconnect');
const spy = jest.spyOn(reconnectService, 'fetchNotificationsOnReconnect');
await reconnectService.handleRouteSpecificFetch();
expect(spy).toHaveBeenCalled();
});
it('should fetch notifications if current route is a notification route', async () => {
isNotificationRoute.mockReturnValue(true);
const spy = vi.spyOn(reconnectService, 'fetchNotificationsOnReconnect');
const spy = jest.spyOn(reconnectService, 'fetchNotificationsOnReconnect');
await reconnectService.handleRouteSpecificFetch();
expect(spy).toHaveBeenCalled();
});
@@ -325,7 +320,7 @@ describe('ReconnectService', () => {
describe('onDisconnect', () => {
it('should set disconnectTime and call setConversationLastMessageId', () => {
reconnectService.setConversationLastMessageId = vi.fn();
reconnectService.setConversationLastMessageId = jest.fn();
reconnectService.onDisconnect();
expect(reconnectService.disconnectTime).toBeInstanceOf(Date);
expect(reconnectService.setConversationLastMessageId).toHaveBeenCalled();
@@ -334,8 +329,8 @@ describe('ReconnectService', () => {
describe('onReconnect', () => {
it('should handle route-specific fetch, revalidate caches, and emit WEBSOCKET_RECONNECT_COMPLETED event', async () => {
reconnectService.handleRouteSpecificFetch = vi.fn();
reconnectService.revalidateCaches = vi.fn();
reconnectService.handleRouteSpecificFetch = jest.fn();
reconnectService.revalidateCaches = jest.fn();
await reconnectService.onReconnect();
expect(reconnectService.handleRouteSpecificFetch).toHaveBeenCalled();
expect(reconnectService.revalidateCaches).toHaveBeenCalled();
@@ -36,14 +36,16 @@ describe('#createPendingMessage', () => {
message: 'hi',
};
it('returns the pending message with expected new keys', () => {
expect(createPendingMessage(message)).toMatchObject({
content: expect.anything(),
id: expect.anything(),
status: expect.anything(),
echo_id: expect.anything(),
created_at: expect.anything(),
message_type: expect.anything(),
});
expect(createPendingMessage(message)).toHaveProperty(
'content',
'id',
'status',
'echo_id',
'status',
'created_at',
'message_type',
'conversation_id'
);
});
it('returns the pending message with status progress', () => {
@@ -59,20 +61,23 @@ describe('#createPendingMessage', () => {
});
});
it('returns the pending message with attachment key if file is passed', () => {
it('returns the pending message with attachmnet key if file is passed', () => {
const messageWithFile = {
message: 'hi',
file: {},
};
expect(createPendingMessage(messageWithFile)).toMatchObject({
content: expect.anything(),
id: expect.anything(),
status: expect.anything(),
echo_id: expect.anything(),
created_at: expect.anything(),
message_type: expect.anything(),
attachments: [{ id: expect.anything() }],
});
expect(createPendingMessage(messageWithFile)).toHaveProperty(
'content',
'id',
'status',
'echo_id',
'status',
'created_at',
'message_type',
'conversation_id',
'attachments',
'private'
);
});
it('returns the pending message to have one attachment', () => {
@@ -19,19 +19,19 @@ describe('resize directive', () => {
beforeEach(() => {
el = document.createElement('div');
binding = {
value: vi.fn(),
value: jest.fn(),
};
observer = {
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
};
window.ResizeObserver = ResizeObserverMock;
vi.spyOn(window, 'ResizeObserver').mockImplementation(() => observer);
jest.spyOn(window, 'ResizeObserver').mockImplementation(() => observer);
});
afterEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
});
it('should create ResizeObserver on bind', () => {
@@ -44,7 +44,7 @@ describe('resize directive', () => {
it('should call callback on observer callback', () => {
el = document.createElement('div');
binding = {
value: vi.fn(),
value: jest.fn(),
};
resize.bind(el, binding);
@@ -283,17 +283,17 @@ describe('findNodeToInsertImage', () => {
mockEditorState = {
selection: {
$from: {
node: vi.fn(() => ({})),
node: jest.fn(() => ({})),
},
from: 0,
},
schema: {
nodes: {
image: {
create: vi.fn(attrs => ({ type: { name: 'image' }, attrs })),
create: jest.fn(attrs => ({ type: { name: 'image' }, attrs })),
},
paragraph: {
create: vi.fn((_, node) => ({
create: jest.fn((_, node) => ({
type: { name: 'paragraph' },
content: [node],
})),
@@ -381,11 +381,11 @@ describe('setURLWithQueryAndSize', () => {
beforeEach(() => {
selectedNode = {
setAttribute: vi.fn(),
setAttribute: jest.fn(),
};
const tr = {
setNodeMarkup: vi.fn().mockReturnValue({
setNodeMarkup: jest.fn().mockReturnValue({
docChanged: true,
}),
};
@@ -397,7 +397,7 @@ describe('setURLWithQueryAndSize', () => {
editorView = {
state,
dispatch: vi.fn(),
dispatch: jest.fn(),
};
});
@@ -420,7 +420,7 @@ describe('setURLWithQueryAndSize', () => {
});
it('does not update the editor view if the document has not changed', () => {
editorView.state.tr.setNodeMarkup = vi.fn().mockReturnValue({
editorView.state.tr.setNodeMarkup = jest.fn().mockReturnValue({
docChanged: false,
});
@@ -1,7 +1,7 @@
import { setColorTheme } from 'dashboard/helper/themeHelper.js';
import { LocalStorage } from 'shared/helpers/localStorage';
vi.mock('shared/helpers/localStorage');
jest.mock('shared/helpers/localStorage');
describe('setColorTheme', () => {
it('should set body class to dark if selectedColorScheme is dark', () => {
@@ -1,8 +1,9 @@
import { uploadFile } from '../uploadHelper';
import axios from 'axios';
// Mocking axios using jest-mock-axios
global.axios = axios;
vi.mock('axios');
jest.mock('axios');
describe('#Upload Helpers', () => {
afterEach(() => {
@@ -494,17 +494,6 @@
"STATUS": "Status"
}
},
"TEAM_CONVERSATIONS": {
"HEADER": "Conversations by teams",
"LOADING_MESSAGE": "Loading team metrics...",
"NO_AGENTS": "There is no data available",
"TABLE_HEADER": {
"AGENT": "Team",
"OPEN": "open",
"UNATTENDED": "Unattended",
"STATUS": "Status"
}
},
"AGENT_STATUS": {
"HEADER": "Agent status",
"ONLINE": "Online",
@@ -4,8 +4,8 @@ import Vuex from 'vuex';
import OpenAPI from '../../api/integrations/openapi';
import { LocalStorage } from '../../../shared/helpers/localStorage';
vi.mock('../../api/integrations/openapi');
vi.mock('../../../shared/helpers/localStorage');
jest.mock('../../api/integrations/openapi');
jest.mock('../../../shared/helpers/localStorage');
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -18,12 +18,12 @@ describe('aiMixin', () => {
let actions;
beforeEach(() => {
OpenAPI.processEvent = vi.fn();
LocalStorage.set = vi.fn();
LocalStorage.get = vi.fn();
OpenAPI.processEvent = jest.fn();
LocalStorage.set = jest.fn();
LocalStorage.get = jest.fn();
actions = {
['integrations/get']: vi.fn(),
['integrations/get']: jest.fn(),
};
getters = {
@@ -63,20 +63,20 @@ describe('aiMixin', () => {
localVue,
});
const dispatchSpy = vi.spyOn(wrapper.vm.$store, 'dispatch');
const dispatchSpy = jest.spyOn(wrapper.vm.$store, 'dispatch');
await wrapper.vm.fetchIntegrationsIfRequired();
expect(dispatchSpy).toHaveBeenCalledWith('integrations/get');
});
it('does not fetch integrations', async () => {
const dispatchSpy = vi.spyOn(wrapper.vm.$store, 'dispatch');
const dispatchSpy = jest.spyOn(wrapper.vm.$store, 'dispatch');
await wrapper.vm.fetchIntegrationsIfRequired();
expect(dispatchSpy).not.toHaveBeenCalledWith('integrations/get');
expect(wrapper.vm.isAIIntegrationEnabled).toBeTruthy();
});
it('fetches label suggestions', async () => {
const processEventSpy = vi.spyOn(OpenAPI, 'processEvent');
const processEventSpy = jest.spyOn(OpenAPI, 'processEvent');
await wrapper.vm.fetchLabelSuggestions({
conversationId: '123',
});
@@ -11,7 +11,7 @@ describe('attributeMixin', () => {
let store;
beforeEach(() => {
actions = { updateUISettings: vi.fn(), toggleSidebarUIState: vi.fn() };
actions = { updateUISettings: jest.fn(), toggleSidebarUIState: jest.fn() };
getters = {
getSelectedChat: () => ({
id: 7165,
@@ -1,13 +1,13 @@
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
import Vue from 'vue';
vi.mock('shared/helpers/FileHelper', () => ({
checkFileSizeLimit: vi.fn(),
jest.mock('shared/helpers/FileHelper', () => ({
checkFileSizeLimit: jest.fn(),
}));
vi.mock('activestorage', () => ({
DirectUpload: vi.fn().mockImplementation(() => ({
create: vi.fn(),
jest.mock('activestorage', () => ({
DirectUpload: jest.fn().mockImplementation(() => ({
create: jest.fn(),
})),
}));
@@ -27,20 +27,20 @@ describe('FileUploadMixin', () => {
vm.currentUser = {
access_token: 'token',
};
vm.$t = vi.fn(message => message);
vm.showAlert = vi.fn();
vm.attachFile = vi.fn();
vm.$t = jest.fn(message => message);
vm.showAlert = jest.fn();
vm.attachFile = jest.fn();
});
it('should call onDirectFileUpload when direct uploads are enabled', () => {
vm.onDirectFileUpload = vi.fn();
vm.onDirectFileUpload = jest.fn();
vm.onFileUpload({});
expect(vm.onDirectFileUpload).toHaveBeenCalledWith({});
});
it('should call onIndirectFileUpload when direct uploads are disabled', () => {
vm.globalConfig.directUploadsEnabled = false;
vm.onIndirectFileUpload = vi.fn();
vm.onIndirectFileUpload = jest.fn();
vm.onFileUpload({});
expect(vm.onIndirectFileUpload).toHaveBeenCalledWith({});
});
@@ -53,7 +53,7 @@ describe('FileUploadMixin', () => {
it('shows an alert if the file size exceeds the maximum limit', () => {
const fakeFile = { size: 999999999 };
vm.showAlert = vi.fn();
vm.showAlert = jest.fn();
vm.onDirectFileUpload(fakeFile);
expect(vm.showAlert).toHaveBeenCalledWith(expect.any(String));
});
@@ -67,7 +67,7 @@ describe('FileUploadMixin', () => {
it('shows an alert if the file size exceeds the maximum limit', () => {
const fakeFile = { size: 999999999 };
vm.showAlert = vi.fn();
vm.showAlert = jest.fn();
vm.onIndirectFileUpload(fakeFile);
expect(vm.showAlert).toHaveBeenCalledWith(expect.any(String));
});
@@ -11,14 +11,14 @@ describe('#messageStamp', () => {
describe('#messageTimestamp', () => {
beforeEach(() => {
vi.useFakeTimers('modern');
jest.useFakeTimers('modern');
const mockDate = new Date(2023, 4, 5);
vi.setSystemTime(mockDate);
jest.setSystemTime(mockDate);
});
afterEach(() => {
vi.useRealTimers();
jest.useRealTimers();
});
it('should return the message date in the specified format if the message was sent in the current year', () => {
@@ -35,7 +35,7 @@ describe('#messageTimestamp', () => {
describe('#dynamicTime', () => {
it('returns correct value', () => {
Date.now = vi.fn(() => new Date(Date.UTC(2023, 1, 14)).valueOf());
Date.now = jest.fn(() => new Date(Date.UTC(2023, 1, 14)).valueOf());
expect(TimeMixin.methods.dynamicTime(1612971343)).toEqual(
'about 2 years ago'
);
@@ -14,7 +14,7 @@ describe('uiSettingsMixin', () => {
let store;
beforeEach(() => {
actions = { updateUISettings: vi.fn(), toggleSidebarUIState: vi.fn() };
actions = { updateUISettings: jest.fn(), toggleSidebarUIState: jest.fn() };
getters = {
getUISettings: () => ({
enter_to_send_enabled: false,
@@ -41,6 +41,7 @@
<script>
import Sidebar from '../../components/layout/Sidebar.vue';
import CommandBar from './commands/commandbar.vue';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal.vue';
import AddAccountModal from 'dashboard/components/layout/sidebarComponents/AddAccountModal.vue';
@@ -49,7 +50,6 @@ import AddLabelModal from 'dashboard/routes/dashboard/settings/labels/AddLabel.v
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import wootConstants from 'dashboard/constants/globals';
const CommandBar = () => import('./commands/commandbar.vue');
export default {
components: {
@@ -15,7 +15,7 @@ describe('AddReminder', () => {
});
it('tests resetValue', () => {
const resetValue = vi.spyOn(wrapper.vm, 'resetValue');
const resetValue = jest.spyOn(wrapper.vm, 'resetValue');
wrapper.vm.content = 'test';
wrapper.vm.date = '08/11/2022';
wrapper.vm.resetValue();
@@ -25,7 +25,7 @@ describe('AddReminder', () => {
});
it('tests optionSelected', () => {
const optionSelected = vi.spyOn(wrapper.vm, 'optionSelected');
const optionSelected = jest.spyOn(wrapper.vm, 'optionSelected');
wrapper.vm.label = '';
wrapper.vm.optionSelected({ target: { value: 'test' } });
expect(wrapper.vm.label).toEqual('test');
@@ -33,7 +33,7 @@ describe('AddReminder', () => {
});
it('tests onAdd', () => {
const onAdd = vi.spyOn(wrapper.vm, 'onAdd');
const onAdd = jest.spyOn(wrapper.vm, 'onAdd');
wrapper.vm.label = 'label';
wrapper.vm.content = 'content';
wrapper.vm.date = '08/11/2022';
@@ -117,7 +117,7 @@
/>
<div class="w-full">
<label>{{ $t('CONTACTS_PAGE.LIST.TABLE_HEADER.SOCIAL_PROFILES') }}</label>
<label> Social Profiles </label>
<div
v-for="socialProfile in socialProfileKeys"
:key="socialProfile.key"
@@ -130,7 +130,7 @@
</span>
<input
v-model="socialProfileUserNames[socialProfile.key]"
class="input-group-field ltr:!rounded-l-none rtl:rounded-r-none !mb-0"
class="input-group-field ltr:rounded-l-none rtl:rounded-r-none !mb-0"
type="text"
/>
</div>
@@ -1,6 +1,6 @@
/* eslint arrow-body-style: 0 */
import { frontendURL } from '../../../helper/URLHelper';
const ConversationView = () => import('./ConversationView.vue');
const ConversationView = () => import('./ConversationView');
export default {
routes: [
@@ -59,12 +59,13 @@
</template>
<script>
import { mapGetters } from 'vuex';
import UpgradePage from './UpgradePage.vue';
import UpgradePage from './UpgradePage';
import { frontendURL } from '../../../../helper/URLHelper';
import Sidebar from 'dashboard/components/layout/Sidebar.vue';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import PortalPopover from '../components/PortalPopover.vue';
import HelpCenterSidebar from '../components/Sidebar/Sidebar.vue';
import CommandBar from 'dashboard/routes/dashboard/commands/commandbar.vue';
import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal.vue';
import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector.vue';
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue';
@@ -72,8 +73,6 @@ import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import portalMixin from '../mixins/portalMixin';
import AddCategory from '../pages/categories/AddCategory.vue';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
const CommandBar = () =>
import('dashboard/routes/dashboard/commands/commandbar.vue');
export default {
components: {
@@ -1,32 +1,30 @@
import HelpCenterLayout from './components/HelpCenterLayout.vue';
import HelpCenterLayout from './components/HelpCenterLayout';
import { getPortalRoute } from './helpers/routeHelper';
const ListAllPortals = () => import('./pages/portals/ListAllPortals.vue');
const NewPortal = () => import('./pages/portals/NewPortal.vue');
const ListAllPortals = () => import('./pages/portals/ListAllPortals');
const NewPortal = () => import('./pages/portals/NewPortal');
const EditPortal = () => import('./pages/portals/EditPortal.vue');
const EditPortalBasic = () => import('./pages/portals/EditPortalBasic.vue');
const EditPortal = () => import('./pages/portals/EditPortal');
const EditPortalBasic = () => import('./pages/portals/EditPortalBasic');
const EditPortalCustomization = () =>
import('./pages/portals/EditPortalCustomization.vue');
import('./pages/portals/EditPortalCustomization');
const EditPortalLocales = () => import('./pages/portals/EditPortalLocales.vue');
const ShowPortal = () => import('./pages/portals/ShowPortal.vue');
const PortalDetails = () => import('./pages/portals/PortalDetails.vue');
const PortalCustomization = () =>
import('./pages/portals/PortalCustomization.vue');
const ShowPortal = () => import('./pages/portals/ShowPortal');
const PortalDetails = () => import('./pages/portals/PortalDetails');
const PortalCustomization = () => import('./pages/portals/PortalCustomization');
const PortalSettingsFinish = () =>
import('./pages/portals/PortalSettingsFinish.vue');
import('./pages/portals/PortalSettingsFinish');
const ListAllCategories = () =>
import('./pages/categories/ListAllCategories.vue');
const NewCategory = () => import('./pages/categories/NewCategory.vue');
const EditCategory = () => import('./pages/categories/EditCategory.vue');
const ListAllCategories = () => import('./pages/categories/ListAllCategories');
const NewCategory = () => import('./pages/categories/NewCategory');
const EditCategory = () => import('./pages/categories/EditCategory');
const ListCategoryArticles = () =>
import('./pages/articles/ListCategoryArticles.vue');
const ListAllArticles = () => import('./pages/articles/ListAllArticles.vue');
import('./pages/articles/ListCategoryArticles');
const ListAllArticles = () => import('./pages/articles/ListAllArticles');
const DefaultPortalArticles = () =>
import('./pages/articles/DefaultPortalArticles.vue');
const NewArticle = () => import('./pages/articles/NewArticle.vue');
const EditArticle = () => import('./pages/articles/EditArticle.vue');
import('./pages/articles/DefaultPortalArticles');
const NewArticle = () => import('./pages/articles/NewArticle');
const EditArticle = () => import('./pages/articles/EditArticle');
const portalRoutes = [
{
@@ -36,7 +36,7 @@
</template>
<script>
const EmojiInput = () => import('shared/components/emoji/EmojiInput.vue');
const EmojiInput = () => import('shared/components/emoji/EmojiInput');
export default {
components: { EmojiInput },
@@ -1,12 +1,12 @@
import Facebook from './channels/Facebook.vue';
import Website from './channels/Website.vue';
import Twitter from './channels/Twitter.vue';
import Api from './channels/Api.vue';
import Email from './channels/Email.vue';
import Sms from './channels/Sms.vue';
import Whatsapp from './channels/Whatsapp.vue';
import Line from './channels/Line.vue';
import Telegram from './channels/Telegram.vue';
import Facebook from './channels/Facebook';
import Website from './channels/Website';
import Twitter from './channels/Twitter';
import Api from './channels/Api';
import Email from './channels/Email';
import Sms from './channels/Sms';
import Whatsapp from './channels/Whatsapp';
import Line from './channels/Line';
import Telegram from './channels/Telegram';
const channelViewList = {
facebook: Facebook,
@@ -1,44 +1,19 @@
<template>
<div class="column overflow-auto">
<metric-card :is-live="false" header="All agents overview">
<woot-summary-reports
key="agent-summary-reports"
class="!p-0"
type="agent"
getter-key="agents/getAgents"
attribute-key="team_id"
action-key="summaryReports/fetchAgentSummaryReports"
summary-key="summaryReports/getAgentSummaryReports"
:download-button-label="$t('REPORT.DOWNLOAD_AGENT_REPORTS')"
/>
</metric-card>
<metric-card
:is-live="false"
header="Agent-wise reports"
class="overflow-visible"
>
<woot-reports
key="agent-reports"
:show-download-button="false"
class="!p-0"
type="agent"
getter-key="agents/getAgents"
action-key="agents/get"
/>
</metric-card>
</div>
<woot-reports
key="agent-reports"
type="agent"
getter-key="agents/getAgents"
action-key="agents/get"
:download-button-label="$t('REPORT.DOWNLOAD_AGENT_REPORTS')"
/>
</template>
<script>
import WootSummaryReports from './components/WootSummaryReports.vue';
import MetricCard from './components/overview/MetricCard.vue';
import WootReports from './components/WootReports.vue';
export default {
components: {
WootReports,
MetricCard,
WootSummaryReports,
},
};
</script>
@@ -1,46 +1,19 @@
<template>
<div class="column overflow-auto">
<metric-card :is-live="false" header="All inboxes overview">
<woot-summary-reports
key="inbox-view-reports"
class="!p-0"
type="inbox"
getter-key="inboxes/getInboxes"
action-key="summaryReports/fetchInboxSummaryReports"
summary-key="summaryReports/getInboxSummaryReports"
:download-button-label="$t('INBOX_REPORTS.DOWNLOAD_INBOX_REPORTS')"
/>
</metric-card>
<metric-card
:is-live="false"
header="Inbox-wise reports"
class="overflow-visible"
>
<woot-reports
key="inbox-reports"
:show-download-button="false"
class="!p-0"
type="inbox"
getter-key="inboxes/getInboxes"
action-key="inboxes/get"
/>
</metric-card>
</div>
<woot-reports
key="inbox-reports"
type="inbox"
getter-key="inboxes/getInboxes"
action-key="inboxes/get"
:download-button-label="$t('INBOX_REPORTS.DOWNLOAD_INBOX_REPORTS')"
/>
</template>
<script>
import WootSummaryReports from './components/WootSummaryReports.vue';
import MetricCard from './components/overview/MetricCard.vue';
import WootReports from './components/WootReports.vue';
export default {
components: {
WootReports,
MetricCard,
WootSummaryReports,
},
mounted() {
this.$store.dispatch('agents/get');
},
};
</script>
@@ -1,13 +1,30 @@
<template>
<div class="flex-1 overflow-auto p-4">
<div class="flex">
<open-conversations />
<div class="flex-1 w-full max-w-full md:w-[35%] md:max-w-[35%] flex">
<div class="flex flex-col md:flex-row items-center">
<div
class="flex-1 w-full max-w-full md:w-[65%] md:max-w-[65%] conversation-metric"
>
<metric-card
:header="$t('OVERVIEW_REPORTS.AGENT_STATUS.HEADER')"
class="flex-1"
:header="$t('OVERVIEW_REPORTS.ACCOUNT_CONVERSATIONS.HEADER')"
:is-loading="uiFlags.isFetchingAccountConversationMetric"
:loading-message="
$t('OVERVIEW_REPORTS.ACCOUNT_CONVERSATIONS.LOADING_MESSAGE')
"
>
<div
v-for="(metric, name, index) in conversationMetrics"
:key="index"
class="metric-content flex-1 min-w-0"
>
<h3 class="heading">
{{ name }}
</h3>
<p class="metric">{{ metric }}</p>
</div>
</metric-card>
</div>
<div class="flex-1 w-full max-w-full md:w-[35%] md:max-w-[35%]">
<metric-card :header="$t('OVERVIEW_REPORTS.AGENT_STATUS.HEADER')">
<div
v-for="(metric, name, index) in agentStatusMetrics"
:key="index"
@@ -24,18 +41,6 @@
<div class="max-w-full flex flex-wrap flex-row ml-auto mr-auto">
<metric-card :header="$t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.HEADER')">
<template #control>
<multiselect-dropdown
class="!mb-0 !w-1/3"
:options="dayFilterOptions"
:selected-item="selectedDayFilter"
multiselector-title=""
multiselector-placeholder="Date filter"
no-search-result="No filter found"
input-placeholder="Search for a filter"
:is-filter="true"
:has-thumbnail="false"
@click="onSelectDateFilter"
/>
<woot-button
icon="arrow-download"
size="small"
@@ -47,7 +52,6 @@
</woot-button>
</template>
<report-heatmap
:selected-day-filter="selectedDayFilter"
:heat-data="accountConversationHeatmap"
:is-loading="uiFlags.isFetchingAccountConversationsHeatmap"
/>
@@ -60,16 +64,7 @@
:agent-metrics="agentConversationMetric"
:page-index="pageIndex"
:is-loading="uiFlags.isFetchingAgentConversationMetric"
/>
</metric-card>
</div>
<div class="row">
<metric-card :header="$t('OVERVIEW_REPORTS.TEAM_CONVERSATIONS.HEADER')">
<team-table
:teams="teams"
:team-metrics="teamConversationMetric"
:page-index="pageIndex"
:is-loading="uiFlags.isFetchingTeamConversationMetric"
@page-change="onPageNumberChange"
/>
</metric-card>
</div>
@@ -77,54 +72,34 @@
</template>
<script>
import { mapGetters } from 'vuex';
import OpenConversations from './components/LiveReports/OpenConversations.vue';
import AgentTable from './components/overview/AgentTable.vue';
import TeamTable from './components/overview/TeamTable.vue';
import MetricCard from './components/overview/MetricCard.vue';
import { OVERVIEW_METRICS } from './constants';
import ReportHeatmap from './components/Heatmap.vue';
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
import endOfDay from 'date-fns/endOfDay';
import getUnixTime from 'date-fns/getUnixTime';
import startOfDay from 'date-fns/startOfDay';
import subDays from 'date-fns/subDays';
import { OVERVIEW_METRICS } from './constants';
const dayFilterOptions = [
{
id: 1,
name: 'Last 7 days',
},
{
id: 2,
name: 'Last 30 days',
},
];
export default {
name: 'LiveReports',
components: {
OpenConversations,
TeamTable,
AgentTable,
MetricCard,
ReportHeatmap,
MultiselectDropdown,
},
data() {
return {
pageIndex: 1,
dayFilterOptions: dayFilterOptions,
selectedDayFilter: dayFilterOptions[0],
};
},
computed: {
...mapGetters({
agentStatus: 'agents/getAgentStatus',
agents: 'agents/getAgents',
teams: 'teams/getTeams',
accountConversationMetric: 'getAccountConversationMetric',
agentConversationMetric: 'getAgentConversationMetric',
teamConversationMetric: 'getTeamConversationMetric',
accountConversationHeatmap: 'getAccountConversationHeatmapData',
uiFlags: 'getOverviewUIFlags',
}),
@@ -138,10 +113,19 @@ export default {
});
return metric;
},
conversationMetrics() {
let metric = {};
Object.keys(this.accountConversationMetric).forEach(key => {
const metricName = this.$t(
`OVERVIEW_REPORTS.ACCOUNT_CONVERSATIONS.${OVERVIEW_METRICS[key]}`
);
metric[metricName] = this.accountConversationMetric[key];
});
return metric;
},
},
mounted() {
this.$store.dispatch('agents/get');
this.$store.dispatch('teams/get');
this.fetchAllData();
this.$emitter.on('fetch_overview_reports', () => {
@@ -149,24 +133,18 @@ export default {
});
},
methods: {
onSelectDateFilter(dayFilter) {
this.selectedDayFilter = dayFilter;
this.fetchHeatmapData();
},
fetchAllData() {
this.$store.dispatch('fetchAgentConversationMetric');
this.$store.dispatch('fetchTeamConversationMetric');
this.fetchAccountConversationMetric();
this.fetchAgentConversationMetric();
this.fetchHeatmapData();
},
downloadHeatmapData() {
let to = endOfDay(new Date());
this.$store.dispatch('downloadAccountConversationHeatmap', {
daysBefore: this.selectedDayFilter?.id === 1 ? 6 : 29,
to: getUnixTime(to),
});
},
fetchHeatmapData() {
if (this.uiFlags.isFetchingAccountConversationsHeatmap) {
return;
@@ -179,9 +157,12 @@ export default {
// and reconcile it with the rest of the data
// this will reduce the load on the server doing number crunching
let to = endOfDay(new Date());
let from = startOfDay(
subDays(to, this.selectedDayFilter?.id === 1 ? 6 : 29)
);
let from = startOfDay(subDays(to, 6));
if (this.accountConversationHeatmap.length) {
to = endOfDay(new Date());
from = startOfDay(to);
}
this.$store.dispatch('fetchAccountConversationHeatmap', {
metric: 'conversations_count',
@@ -191,6 +172,21 @@ export default {
businessHours: false,
});
},
fetchAccountConversationMetric() {
this.$store.dispatch('fetchAccountConversationMetric', {
type: 'account',
});
},
fetchAgentConversationMetric() {
this.$store.dispatch('fetchAgentConversationMetric', {
type: 'agent',
page: this.pageIndex,
});
},
onPageNumberChange(pageIndex) {
this.pageIndex = pageIndex;
this.fetchAgentConversationMetric();
},
},
};
</script>
@@ -1,47 +1,19 @@
<template>
<div class="column overflow-auto">
<metric-card :is-live="false" header="All teams overview">
<woot-summary-reports
key="team-reports"
class="!p-0"
type="team"
getter-key="teams/getTeams"
attribute-key="team_id"
action-key="summaryReports/fetchTeamSummaryReports"
summary-key="summaryReports/getTeamSummaryReports"
:download-button-label="$t('TEAM_REPORTS.DOWNLOAD_TEAM_REPORTS')"
/>
</metric-card>
<metric-card
:is-live="false"
header="Team-wise reports"
class="overflow-visible"
>
<woot-reports
key="team-reports"
:show-download-button="false"
class="!p-0"
type="team"
getter-key="teams/getTeams"
action-key="teams/get"
/>
</metric-card>
</div>
<woot-reports
key="team-reports"
type="team"
getter-key="teams/getTeams"
action-key="teams/get"
:download-button-label="$t('TEAM_REPORTS.DOWNLOAD_TEAM_REPORTS')"
/>
</template>
<script>
import WootSummaryReports from './components/WootSummaryReports.vue';
import MetricCard from './components/overview/MetricCard.vue';
import WootReports from './components/WootReports.vue';
export default {
components: {
WootReports,
MetricCard,
WootSummaryReports,
},
mounted() {
this.$store.dispatch('agents/get');
},
};
</script>
@@ -1,141 +0,0 @@
<template>
<div class="agent-table-container">
<ve-table
max-height="calc(100vh - 21.875rem)"
:fixed-header="true"
:columns="columns"
:table-data="tableData"
/>
</div>
</template>
<script>
import { VeTable } from 'vue-easytable';
import rtlMixin from 'shared/mixins/rtlMixin';
import { mapGetters } from 'vuex';
export default {
name: 'TeamTable',
components: {
VeTable,
},
mixins: [rtlMixin],
computed: {
...mapGetters({
teams: 'teams/getTeams',
teamMetrics: 'summaryReports/getTeamSummaryReports',
}),
columns() {
return [
{
field: 'agent',
key: 'agent',
title: 'Team',
fixed: 'left',
align: this.isRTLView ? 'right' : 'left',
width: 25,
renderBodyCell: ({ row }) => (
<div class="row-user-block">
<div class="user-block">
<h6 class="title overflow-hidden whitespace-nowrap text-ellipsis">
{row.name}
</h6>
</div>
</div>
),
},
{
field: 'conversationsCount',
key: 'conversationsCount',
title: 'No. of conversations',
align: this.isRTLView ? 'right' : 'left',
width: 20,
},
{
field: 'resolutionsCount',
key: 'resolutionsCount',
title: 'No. of resolved conversations',
align: this.isRTLView ? 'right' : 'left',
width: 20,
},
{
field: 'avgFirstResponseTime',
key: 'avgFirstResponseTime',
title: 'Average first response time',
align: this.isRTLView ? 'right' : 'left',
width: 20,
},
{
field: 'avgResolutionTime',
key: 'avgResolutionTime',
title: 'Average resolution time',
align: this.isRTLView ? 'right' : 'left',
width: 20,
},
];
},
},
mounted() {
this.$store.dispatch('summaryReports/fetchTeamSummaryReports');
},
methods: {
getTeamMetrics(id) {
return (
this.teamMetrics.find(metrics => metrics.team_id === Number(id)) || {}
);
},
},
};
</script>
<style lang="scss" scoped>
.agent-table-container {
@apply flex flex-col flex-1;
.ve-table {
&::v-deep {
th.ve-table-header-th {
font-size: var(--font-size-mini) !important;
padding: var(--space-small) var(--space-two) !important;
}
td.ve-table-body-td {
padding: var(--space-one) var(--space-two) !important;
}
}
}
&::v-deep .ve-pagination {
@apply bg-transparent dark:bg-transparent;
}
&::v-deep .ve-pagination-select {
@apply hidden;
}
.row-user-block {
@apply items-center flex text-left;
.user-block {
@apply items-start flex flex-col min-w-0 my-0 mx-2;
.title {
@apply text-sm m-0 leading-[1.2] text-slate-800 dark:text-slate-100;
}
.sub-title {
@apply text-xs text-slate-600 dark:text-slate-200;
}
}
}
.table-pagination {
@apply mt-4 text-right;
}
}
.agents-loader {
@apply items-center flex text-base justify-center p-8;
}
</style>
@@ -1,87 +0,0 @@
<script>
import { mapGetters } from 'vuex';
import { OVERVIEW_METRICS } from '../../constants';
import MetricCard from '../overview/MetricCard.vue';
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
const noneTeam = { team_id: 0, name: 'All teams' };
export default {
components: {
MultiselectDropdown,
MetricCard,
},
data() {
return {
selectedTeam: noneTeam,
};
},
computed: {
...mapGetters({
teams: 'teams/getTeams',
accountConversationMetric: 'getAccountConversationMetric',
uiFlags: 'getOverviewUIFlags',
}),
conversationMetrics() {
let metric = {};
Object.keys(this.accountConversationMetric).forEach(key => {
const metricName = this.$t(
`OVERVIEW_REPORTS.ACCOUNT_CONVERSATIONS.${OVERVIEW_METRICS[key]}`
);
metric[metricName] = this.accountConversationMetric[key];
});
return metric;
},
teamsList() {
return [noneTeam, ...this.teams];
},
},
mounted() {
this.$store.dispatch('fetchLiveConversationMetric');
},
methods: {
onSelectTeam(team) {
this.$store.dispatch('fetchLiveConversationMetric', {
team_id: !team.id ? null : team.id,
});
this.selectedTeam = team;
},
},
};
</script>
<template>
<div
class="flex-1 w-full max-w-full md:w-[65%] md:max-w-[65%] conversation-metric"
>
<metric-card
class="!overflow-visible min-h-[150px]"
:header="$t('OVERVIEW_REPORTS.ACCOUNT_CONVERSATIONS.HEADER')"
:is-loading="uiFlags.isFetchingAccountConversationMetric"
loading-message="Loading metrics"
>
<template #control>
<multiselect-dropdown
class="!mb-0 !w-1/2"
:options="teamsList"
:selected-item="selectedTeam"
multiselector-title=""
multiselector-placeholder="All teams"
no-search-result="No teams found"
input-placeholder="Search for a team"
:is-filter="true"
@click="onSelectTeam"
/>
</template>
<div
v-for="(metric, name, index) in conversationMetrics"
:key="index"
class="metric-content column"
>
<h3 class="heading">
{{ name }}
</h3>
<p class="metric">{{ metric }}</p>
</div>
</metric-card>
</div>
</template>
@@ -110,7 +110,6 @@
v-model="currentSelectedFilter"
track-by="id"
label="name"
open-direction="below"
:placeholder="multiselectLabel"
selected-label
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
@@ -128,7 +127,6 @@
<multiselect
v-model="currentDateRangeSelection"
track-by="name"
open-direction="below"
label="name"
:placeholder="$t('FORMS.MULTISELECT.SELECT_ONE')"
selected-label
@@ -1,7 +1,6 @@
<template>
<div class="flex-1 overflow-auto p-4">
<woot-button
v-if="showDownloadButton"
color-scheme="success"
class-names="button--fixed-top"
icon="arrow-download"
@@ -65,10 +64,6 @@ export default {
type: String,
default: 'Download Reports',
},
showDownloadButton: {
type: Boolean,
default: true,
},
},
data() {
return {
@@ -1,180 +0,0 @@
<template>
<div class="flex-1 overflow-auto p-4">
<woot-button
color-scheme="success"
class-names="button--fixed-top"
icon="arrow-download"
@click="downloadReports"
>
{{ downloadButtonLabel }}
</woot-button>
<report-filter-selector
:show-agents-filter="false"
:show-group-by-filter="false"
@filter-change="onFilterChange"
/>
<ve-table
max-height="calc(100vh - 21.875rem)"
:fixed-header="true"
:columns="columns"
:table-data="tableData"
/>
</div>
</template>
<script>
import ReportFilterSelector from './FilterSelector.vue';
import { formatTime } from '@chatwoot/utils';
import reportMixin from '../../../../../mixins/reportMixin';
import { generateFileName } from '../../../../../helper/downloadHelper';
import { VeTable } from 'vue-easytable';
export default {
components: {
VeTable,
ReportFilterSelector,
},
mixins: [reportMixin],
props: {
type: {
type: String,
default: 'account',
},
getterKey: {
type: String,
default: '',
},
actionKey: {
type: String,
default: '',
},
summaryKey: {
type: String,
default: '',
},
downloadButtonLabel: {
type: String,
default: 'Download Reports',
},
},
data() {
return {
from: 0,
to: 0,
selectedFilter: null,
businessHours: false,
};
},
computed: {
columns() {
return [
{
field: 'agent',
key: 'agent',
title: this.type,
fixed: 'left',
align: this.isRTLView ? 'right' : 'left',
width: 25,
renderBodyCell: ({ row }) => (
<div class="row-user-block">
<div class="user-block">
<h6 class="title overflow-hidden whitespace-nowrap text-ellipsis text-sm capitalize">
{row.name}
</h6>
</div>
</div>
),
},
{
field: 'conversationsCount',
key: 'conversationsCount',
title: 'Assigned',
align: this.isRTLView ? 'right' : 'left',
width: 20,
},
{
field: 'resolutionsCount',
key: 'resolutionsCount',
title: 'Resolved',
align: this.isRTLView ? 'right' : 'left',
width: 20,
},
{
field: 'avgFirstResponseTime',
key: 'avgFirstResponseTime',
title: 'Avg. first response time',
align: this.isRTLView ? 'right' : 'left',
width: 20,
},
{
field: 'avgResolutionTime',
key: 'avgResolutionTime',
title: 'Avg. resolution time',
align: this.isRTLView ? 'right' : 'left',
width: 20,
},
];
},
tableData() {
return this.filterItemsList.map(team => {
const typeMetrics = this.getMetrics(team.id);
return {
name: team.name,
conversationsCount: typeMetrics.conversations_count || '--',
avgFirstResponseTime:
this.renderContent(typeMetrics.avg_first_response_time) || '--',
avgResolutionTime:
this.renderContent(typeMetrics.avg_resolution_time) || '--',
resolutionsCount: typeMetrics.resolved_conversations_count || '--',
};
});
},
filterItemsList() {
return this.$store.getters[this.getterKey] || [];
},
typeMetrics() {
return this.$store.getters[this.summaryKey] || [];
},
},
mounted() {
this.fetchAllData();
},
methods: {
renderContent(value) {
return value ? formatTime(value) : '--';
},
getMetrics(id) {
return this.typeMetrics.find(metrics => metrics.id === Number(id)) || {};
},
fetchAllData() {
const { from, to, businessHours } = this;
this.$store.dispatch(this.actionKey, {
since: from,
until: to,
businessHours,
});
},
downloadReports() {
const { from, to, type, businessHours } = this;
const dispatchMethods = {
agent: 'downloadAgentReports',
label: 'downloadLabelReports',
inbox: 'downloadInboxReports',
team: 'downloadTeamReports',
};
if (dispatchMethods[type]) {
const fileName = generateFileName({ type, to, businessHours });
const params = { from, to, fileName, businessHours };
this.$store.dispatch(dispatchMethods[type], params);
}
},
onFilterChange({ from, to, businessHours }) {
this.from = from;
this.to = to;
this.businessHours = businessHours;
this.fetchAllData();
},
},
};
</script>
@@ -12,12 +12,17 @@
$t('OVERVIEW_REPORTS.AGENT_CONVERSATIONS.LOADING_MESSAGE')
}}</span>
</div>
<div v-if="agents.length > 0" class="table-pagination">
<empty-state
v-else-if="!isLoading && !agentMetrics.length"
:title="$t('OVERVIEW_REPORTS.AGENT_CONVERSATIONS.NO_AGENTS')"
/>
<div v-if="agentMetrics.length > 0" class="table-pagination">
<ve-pagination
:total="agents.length"
:page-index="pageIndex"
:page-size="25"
:page-size-option="[25]"
@on-page-number-change="onPageNumberChange"
/>
</div>
</div>
@@ -26,12 +31,14 @@
<script>
import { VeTable, VePagination } from 'vue-easytable';
import Spinner from 'shared/components/Spinner.vue';
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
import rtlMixin from 'shared/mixins/rtlMixin';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
export default {
name: 'AgentTable',
components: {
EmptyState,
Spinner,
VeTable,
VePagination,
@@ -57,17 +64,19 @@ export default {
},
computed: {
tableData() {
return this.agents.map(agent => {
const agentMetrics = this.getAgentMetrics(agent.id);
return {
agent: agent.name,
email: agent.email,
thumbnail: agent.thumbnail,
open: agentMetrics.open || 0,
unattended: agentMetrics.unattended || 0,
status: agent.availability_status,
};
});
return this.agentMetrics
.filter(agentMetric => this.getAgentInformation(agentMetric.id))
.map(agent => {
const agentInformation = this.getAgentInformation(agent.id);
return {
agent: agentInformation.name || agentInformation.available_name,
email: agentInformation.email,
thumbnail: agentInformation.thumbnail,
open: agent.metric.open || 0,
unattended: agent.metric.unattended || 0,
status: agentInformation.availability_status,
};
});
},
columns() {
return [
@@ -119,15 +128,12 @@ export default {
},
},
methods: {
getAgentMetrics(id) {
return (
this.agentMetrics.find(metrics => metrics.assignee_id === Number(id)) ||
{}
);
},
onPageNumberChange(pageIndex) {
this.$emit('page-change', pageIndex);
},
getAgentInformation(id) {
return this.agents?.find(agent => agent.id === Number(id));
},
},
};
</script>
@@ -166,7 +172,6 @@ export default {
.title {
@apply text-sm m-0 leading-[1.2] text-slate-800 dark:text-slate-100;
}
.sub-title {
@apply text-xs text-slate-600 dark:text-slate-200;
}
@@ -11,7 +11,6 @@
{{ header }}
</h5>
<span
v-if="isLive"
class="flex flex-row items-center pr-2 pl-2 m-1 rounded-sm text-green-400 dark:text-green-400 text-xs bg-green-100/30 dark:bg-green-100/20"
>
<span
@@ -30,7 +29,6 @@
<div
v-if="!isLoading"
class="card-body max-w-full w-full ml-auto mr-auto justify-between flex"
:class="!isLive ? 'px-0' : ''"
>
<slot />
</div>
@@ -54,10 +52,6 @@ export default {
Spinner,
},
props: {
isLive: {
type: Boolean,
default: true,
},
header: {
type: String,
default: '',
@@ -70,10 +64,6 @@ export default {
type: String,
default: '',
},
isFilter: {
type: Boolean,
default: false,
},
},
};
</script>
@@ -106,18 +96,12 @@ export default {
.card-body {
.metric-content {
@apply pb-2;
.heading {
@apply text-base text-slate-700 dark:text-slate-100;
}
.metric {
@apply text-woot-800 dark:text-woot-300 text-3xl mb-0 mt-1;
}
}
}
.conversation-metric-loader {
@apply items-center flex text-base justify-center;
}
</style>
@@ -1,169 +0,0 @@
<template>
<div class="agent-table-container">
<ve-table
max-height="calc(100vh - 21.875rem)"
:fixed-header="true"
:columns="columns"
:table-data="tableData"
/>
<div v-if="isLoading" class="agents-loader">
<spinner />
<span>{{
$t('OVERVIEW_REPORTS.TEAM_CONVERSATIONS.LOADING_MESSAGE')
}}</span>
</div>
<div v-if="teams.length > 0" class="table-pagination">
<ve-pagination
:total="teams.length"
:page-index="pageIndex"
:page-size="25"
:page-size-option="[25]"
/>
</div>
</div>
</template>
<script>
import { VeTable, VePagination } from 'vue-easytable';
import Spinner from 'shared/components/Spinner.vue';
import rtlMixin from 'shared/mixins/rtlMixin';
export default {
name: 'TeamTable',
components: {
Spinner,
VeTable,
VePagination,
},
mixins: [rtlMixin],
props: {
teams: {
type: Array,
default: () => [],
},
teamMetrics: {
type: Array,
default: () => [],
},
isLoading: {
type: Boolean,
default: false,
},
pageIndex: {
type: Number,
default: 1,
},
},
computed: {
tableData() {
return this.teams.map(team => {
const teamMetrics = this.getTeamMetrics(team.id);
return {
agent: team.name,
open: teamMetrics.open || 0,
unattended: teamMetrics.unattended || 0,
};
});
},
columns() {
return [
{
field: 'agent',
key: 'agent',
title: this.$t(
'OVERVIEW_REPORTS.TEAM_CONVERSATIONS.TABLE_HEADER.AGENT'
),
fixed: 'left',
align: this.isRTLView ? 'right' : 'left',
width: 25,
renderBodyCell: ({ row }) => (
<div class="row-user-block">
<div class="user-block">
<h6 class="capitalize title overflow-hidden whitespace-nowrap text-ellipsis">
{row.agent}
</h6>
</div>
</div>
),
},
{
field: 'open',
key: 'open',
title: this.$t(
'OVERVIEW_REPORTS.TEAM_CONVERSATIONS.TABLE_HEADER.OPEN'
),
align: this.isRTLView ? 'right' : 'left',
width: 10,
},
{
field: 'unattended',
key: 'unattended',
title: this.$t(
'OVERVIEW_REPORTS.TEAM_CONVERSATIONS.TABLE_HEADER.UNATTENDED'
),
align: this.isRTLView ? 'right' : 'left',
width: 10,
},
];
},
},
methods: {
getTeamMetrics(id) {
return (
this.teamMetrics.find(metrics => metrics.team_id === Number(id)) || {}
);
},
},
};
</script>
<style lang="scss" scoped>
.agent-table-container {
@apply flex flex-col flex-1;
.ve-table {
&::v-deep {
th.ve-table-header-th {
font-size: var(--font-size-mini) !important;
padding: var(--space-small) var(--space-two) !important;
}
td.ve-table-body-td {
padding: var(--space-one) var(--space-two) !important;
}
}
}
&::v-deep .ve-pagination {
@apply bg-transparent dark:bg-transparent;
}
&::v-deep .ve-pagination-select {
@apply hidden;
}
.row-user-block {
@apply items-center flex text-left;
.user-block {
@apply items-start flex flex-col min-w-0 my-0 mx-2;
.title {
@apply text-sm m-0 leading-[1.2] text-slate-800 dark:text-slate-100;
}
.sub-title {
@apply text-xs text-slate-600 dark:text-slate-200;
}
}
}
.table-pagination {
@apply mt-4 text-right;
}
}
.agents-loader {
@apply items-center flex text-base justify-center p-8;
}
</style>
@@ -1,6 +1,6 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import ReportsFiltersAgents from '../../Filters/Agents.vue';
import ReportsFiltersAgents from '../../Filters/Agents';
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -16,7 +16,7 @@ const mockStore = new Vuex.Store({
getAgents: state => state.agents,
},
actions: {
get: vi.fn(),
get: jest.fn(),
},
},
},
@@ -50,7 +50,7 @@ describe('ReportsFiltersAgents.vue', () => {
});
it('dispatches the "agents/get" action when the component is mounted', () => {
const dispatchSpy = vi.spyOn(mockStore, 'dispatch');
const dispatchSpy = jest.spyOn(mockStore, 'dispatch');
shallowMount(ReportsFiltersAgents, mountParams);
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import ReportsFiltersDateGroupBy from '../../Filters/DateGroupBy.vue';
import ReportsFiltersDateGroupBy from '../../Filters/DateGroupBy';
import { GROUP_BY_OPTIONS } from '../../../constants';
const mountParams = {
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import ReportFiltersDateRange from '../../Filters/DateRange.vue';
import ReportFiltersDateRange from '../../Filters/DateRange';
import { DATE_RANGE_OPTIONS } from '../../../constants';
const mountParams = {
@@ -1,6 +1,6 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import ReportsFiltersInboxes from '../../Filters/Inboxes.vue';
import ReportsFiltersInboxes from '../../Filters/Inboxes';
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -26,7 +26,7 @@ describe('ReportsFiltersInboxes.vue', () => {
],
},
actions: {
get: vi.fn(),
get: jest.fn(),
},
};
@@ -1,6 +1,6 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import ReportsFiltersLabels from '../../Filters/Labels.vue';
import ReportsFiltersLabels from '../../Filters/Labels';
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -26,7 +26,7 @@ describe('ReportsFiltersLabels.vue', () => {
],
},
actions: {
get: vi.fn(),
get: jest.fn(),
},
};
@@ -1,5 +1,5 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import ReportFiltersRatings from '../../Filters/Ratings.vue';
import ReportFiltersRatings from '../../Filters/Ratings';
import { CSAT_RATINGS } from 'shared/constants/messages';
const mountParams = {
@@ -26,7 +26,7 @@ describe('ReportsFiltersTeams.vue', () => {
],
},
actions: {
get: vi.fn(),
get: jest.fn(),
},
};
@@ -1,10 +1,10 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CsatMetrics.vue > computes response count correctly 1`] = `
"<div class="flex-col lg:flex-row flex flex-wrap mx-0 bg-white dark:bg-slate-800 rounded-[4px] p-4 mb-5 border border-solid border-slate-75 dark:border-slate-700">
exports[`CsatMetrics.vue computes response count correctly 1`] = `
<div class="flex-col lg:flex-row flex flex-wrap mx-0 bg-white dark:bg-slate-800 rounded-[4px] p-4 mb-5 border border-solid border-slate-75 dark:border-slate-700">
<csat-metric-card-stub label="CSAT_REPORTS.METRIC.TOTAL_RESPONSES.LABEL" value="100" infotext="CSAT_REPORTS.METRIC.TOTAL_RESPONSES.TOOLTIP" class="xs:w-full sm:max-w-[50%] lg:w-1/6 lg:max-w-[16%]"></csat-metric-card-stub>
<csat-metric-card-stub label="CSAT_REPORTS.METRIC.SATISFACTION_SCORE.LABEL" value="--" infotext="CSAT_REPORTS.METRIC.SATISFACTION_SCORE.TOOLTIP" disabled="true" class="xs:w-full sm:max-w-[50%] lg:w-1/6 lg:max-w-[16%]"></csat-metric-card-stub>
<csat-metric-card-stub label="CSAT_REPORTS.METRIC.RESPONSE_RATE.LABEL" value="90%" infotext="CSAT_REPORTS.METRIC.RESPONSE_RATE.TOOLTIP" class="xs:w-full sm:max-w-[50%] lg:w-1/6 lg:max-w-[16%]"></csat-metric-card-stub>
<!---->
</div>"
</div>
`;
+15 -16
View File
@@ -1,11 +1,18 @@
import 'expect-more-jest';
import { validateAuthenticateRoutePermission } from './index';
jest.mock('./dashboard/dashboard.routes', () => ({
routes: [],
}));
window.roleWiseRoutes = {};
describe('#validateAuthenticateRoutePermission', () => {
describe(`when route is protected`, () => {
describe(`when user not logged in`, () => {
it(`should redirect to login`, () => {
// Arrange
const to = { name: 'some-protected-route', params: { accountId: 1 } };
const next = vi.fn();
const next = jest.fn();
const getters = {
isLoggedIn: false,
getCurrentUser: {
@@ -23,18 +30,14 @@ describe('#validateAuthenticateRoutePermission', () => {
describe(`when user is logged in`, () => {
describe(`when route is not accessible to current user`, () => {
it(`should redirect to dashboard`, () => {
const to = {
name: 'general_settings_index',
params: { accountId: 1 },
meta: { permissions: ['administrator'] },
};
const next = vi.fn();
window.roleWiseRoutes.agent = ['dashboard'];
const to = { name: 'admin', params: { accountId: 1 } };
const next = jest.fn();
const getters = {
isLoggedIn: true,
getCurrentUser: {
account_id: 1,
id: 1,
permissions: ['agent'],
accounts: [{ id: 1, role: 'agent', status: 'active' }],
},
};
@@ -44,19 +47,15 @@ describe('#validateAuthenticateRoutePermission', () => {
});
describe(`when route is accessible to current user`, () => {
it(`should go there`, () => {
const to = {
name: 'general_settings_index',
params: { accountId: 1 },
meta: { permissions: ['administrator'] },
};
const next = vi.fn();
window.roleWiseRoutes.agent = ['dashboard', 'admin'];
const to = { name: 'admin', params: { accountId: 1 } };
const next = jest.fn();
const getters = {
isLoggedIn: true,
getCurrentUser: {
account_id: 1,
id: 1,
permissions: ['administrator'],
accounts: [{ id: 1, role: 'administrator', status: 'active' }],
accounts: [{ id: 1, role: 'agent', status: 'active' }],
},
};
validateAuthenticateRoutePermission(to, next, { getters });
-2
View File
@@ -42,7 +42,6 @@ import sla from './modules/sla';
import teamMembers from './modules/teamMembers';
import teams from './modules/teams';
import userNotificationSettings from './modules/userNotificationSettings';
import summaryReports from './modules/summaryReports';
import webhooks from './modules/webhooks';
import draftMessages from './modules/draftMessages';
import SLAReports from './modules/SLAReports';
@@ -91,7 +90,6 @@ export default new Vuex.Store({
reports,
teamMembers,
teams,
summaryReports,
userNotificationSettings,
webhooks,
draftMessages,
@@ -3,7 +3,7 @@ import { actions } from '../actions';
import * as types from '../../../mutation-types';
import { uploadFile } from 'dashboard/helper/uploadHelper';
vi.mock('dashboard/helper/uploadHelper');
jest.mock('dashboard/helper/uploadHelper');
const articleList = [
{
@@ -12,10 +12,10 @@ const articleList = [
title: 'Documents are required to complete KYC',
},
];
const commit = vi.fn();
const dispatch = vi.fn();
const commit = jest.fn();
const dispatch = jest.fn();
global.axios = axios;
vi.mock('axios');
jest.mock('axios');
describe('#actions', () => {
describe('#index', () => {
@@ -2,9 +2,9 @@ import axios from 'axios';
import { actions } from '../actions';
import * as types from '../../../mutation-types';
import { categoriesPayload } from './fixtures';
const commit = vi.fn();
const commit = jest.fn();
global.axios = axios;
vi.mock('axios');
jest.mock('axios');
describe('#actions', () => {
describe('#index', () => {
@@ -3,10 +3,10 @@ import { actions } from '../actions';
import { types } from '../mutations';
import { apiResponse } from './fixtures';
const commit = vi.fn();
const dispatch = vi.fn();
const commit = jest.fn();
const dispatch = jest.fn();
global.axios = axios;
vi.mock('axios');
jest.mock('axios');
describe('#actions', () => {
describe('#index', () => {
@@ -4,19 +4,10 @@ import Report from '../../api/reports';
import { downloadCsvFile, generateFileName } from '../../helper/downloadHelper';
import AnalyticsHelper from '../../helper/AnalyticsHelper';
import { REPORTS_EVENTS } from '../../helper/AnalyticsHelper/events';
import { clampDataBetweenTimeline } from 'shared/helpers/ReportsDataHelper';
import liveReports from '../../api/liveReports';
const accountSummaryInitialData = {
avg_first_response_time: 0,
avg_resolution_time: 0,
conversations_count: 0,
incoming_messages_count: 0,
outgoing_messages_count: 0,
reply_time: 0,
resolutions_count: 0,
previous: {},
};
import {
reconcileHeatmapData,
clampDataBetweenTimeline,
} from 'shared/helpers/ReportsDataHelper';
const state = {
fetchingStatus: false,
@@ -44,7 +35,18 @@ const state = {
reply_time: [],
},
},
accountSummary: accountSummaryInitialData,
accountSummary: {
avg_first_response_time: 0,
avg_resolution_time: 0,
conversations_count: 0,
incoming_messages_count: 0,
outgoing_messages_count: 0,
reply_time: 0,
resolutions_count: 0,
bot_resolutions_count: 0,
bot_handoffs_count: 0,
previous: {},
},
botSummary: {
bot_resolutions_count: 0,
bot_handoffs_count: 0,
@@ -55,12 +57,10 @@ const state = {
isFetchingAccountConversationMetric: false,
isFetchingAccountConversationsHeatmap: false,
isFetchingAgentConversationMetric: false,
isFetchingTeamConversationMetric: false,
},
accountConversationMetric: {},
accountConversationHeatmap: [],
agentConversationMetric: [],
teamConversationMetric: [],
},
};
@@ -83,9 +83,6 @@ const getters = {
getAgentConversationMetric(_state) {
return _state.overview.agentConversationMetric;
},
getTeamConversationMetric(_state) {
return _state.overview.teamConversationMetric;
},
getOverviewUIFlags($state) {
return $state.overview.uiFlags;
},
@@ -117,12 +114,16 @@ export const actions = {
let { data } = heatmapData;
data = clampDataBetweenTimeline(data, reportObj.from, reportObj.to);
data = reconcileHeatmapData(
data,
state.overview.accountConversationHeatmap
);
commit(types.default.SET_HEATMAP_DATA, data);
commit(types.default.TOGGLE_HEATMAP_LOADING, false);
});
},
fetchAccountSummary({ commit }, reportObj) {
commit(types.default.SET_ACCOUNT_SUMMARY, accountSummaryInitialData);
Report.getSummary(
reportObj.from,
reportObj.to,
@@ -152,10 +153,9 @@ export const actions = {
commit(types.default.TOGGLE_ACCOUNT_REPORT_LOADING, false);
});
},
fetchLiveConversationMetric({ commit }, params = {}) {
fetchAccountConversationMetric({ commit }, reportObj) {
commit(types.default.TOGGLE_ACCOUNT_CONVERSATION_METRIC_LOADING, true);
liveReports
.getConversationMetric(params)
Report.getConversationMetric(reportObj.type)
.then(accountConversationMetric => {
commit(
types.default.SET_ACCOUNT_CONVERSATION_METRIC,
@@ -167,10 +167,9 @@ export const actions = {
commit(types.default.TOGGLE_ACCOUNT_CONVERSATION_METRIC_LOADING, false);
});
},
fetchAgentConversationMetric({ commit }) {
fetchAgentConversationMetric({ commit }, reportObj) {
commit(types.default.TOGGLE_AGENT_CONVERSATION_METRIC_LOADING, true);
liveReports
.getGroupedConversations()
Report.getConversationMetric(reportObj.type, reportObj.page)
.then(agentConversationMetric => {
commit(
types.default.SET_AGENT_CONVERSATION_METRIC,
@@ -182,21 +181,6 @@ export const actions = {
commit(types.default.TOGGLE_AGENT_CONVERSATION_METRIC_LOADING, false);
});
},
fetchTeamConversationMetric({ commit }) {
commit(types.default.TOGGLE_TEAM_CONVERSATION_METRIC_LOADING, true);
liveReports
.getGroupedConversations({ groupBy: 'team_id' })
.then(agentConversationMetric => {
commit(
types.default.SET_TEAM_CONVERSATION_METRIC,
agentConversationMetric.data
);
commit(types.default.TOGGLE_TEAM_CONVERSATION_METRIC_LOADING, false);
})
.catch(() => {
commit(types.default.TOGGLE_TEAM_CONVERSATION_METRIC_LOADING, false);
});
},
downloadAgentReports(_, reportObj) {
return Report.getAgentReports(reportObj)
.then(response => {
@@ -250,7 +234,7 @@ export const actions = {
});
},
downloadAccountConversationHeatmap(_, reportObj) {
Report.getConversationTrafficCSV({ daysBefore: reportObj.daysBefore })
Report.getConversationTrafficCSV()
.then(response => {
downloadCsvFile(
generateFileName({
@@ -299,15 +283,9 @@ const mutations = {
[types.default.SET_AGENT_CONVERSATION_METRIC](_state, metricData) {
_state.overview.agentConversationMetric = metricData;
},
[types.default.SET_TEAM_CONVERSATION_METRIC](_state, metricData) {
_state.overview.teamConversationMetric = metricData;
},
[types.default.TOGGLE_AGENT_CONVERSATION_METRIC_LOADING](_state, flag) {
_state.overview.uiFlags.isFetchingAgentConversationMetric = flag;
},
[types.default.TOGGLE_TEAM_CONVERSATION_METRIC_LOADING](_state, flag) {
_state.overview.uiFlags.isFetchingTeamConversationMetric = flag;
},
};
export default {

Some files were not shown because too many files have changed in this diff Show More