Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fb043de54 | ||
|
|
6d07a7d92e | ||
|
|
7b9a9831c1 | ||
|
|
7ab7bac6bf | ||
|
|
b988a01df3 | ||
|
|
105f9a27d2 | ||
|
|
132cf802d5 | ||
|
|
f2f33038f2 | ||
|
|
590ce788b9 | ||
|
|
a25179c342 | ||
|
|
aea9470b6a | ||
|
|
6382707146 | ||
|
|
49ef4e54ca | ||
|
|
db0d32f01d |
+23
-31
@@ -162,41 +162,33 @@ Rails/RenderInline:
|
||||
Performance/CollectionLiteralInLoop:
|
||||
Exclude:
|
||||
- 'db/migrate/20210315101919_enable_email_channel.rb'
|
||||
RSpec/NamedSubject:
|
||||
Enabled: false
|
||||
Style/RedundantConstantBase:
|
||||
Enabled: false
|
||||
Rails/RootPathnameMethods:
|
||||
Enabled: false
|
||||
RSpec/Rails/MinitestAssertions:
|
||||
Enabled: false
|
||||
RSpec/Rails/InferredSpecType:
|
||||
Enabled: false
|
||||
Rails/ThreeStateBooleanColumn:
|
||||
Exclude:
|
||||
- 'db/migrate/20200509044639_add_hide_input_flag_to_bot_config.rb'
|
||||
- 'db/migrate/20200605130625_agent_away_message_to_auto_reply.rb'
|
||||
- 'db/migrate/20200606132552_create_labels.rb'
|
||||
- 'db/migrate/20201027135006_create_working_hours.rb'
|
||||
- 'db/migrate/20210112174124_add_hmac_token_to_inbox.rb'
|
||||
- 'db/migrate/20210114202310_create_teams.rb'
|
||||
- 'db/migrate/20210212154240_add_request_for_email_on_channel_web_widget.rb'
|
||||
- 'db/migrate/20210428135041_add_campaigns.rb'
|
||||
- 'db/migrate/20210602182058_add_hmac_to_api_channel.rb'
|
||||
- 'db/migrate/20210609133433_add_email_collect_to_inboxes.rb'
|
||||
- 'db/migrate/20210618095823_add_csat_toggle_for_inbox.rb'
|
||||
- 'db/migrate/20210927062350_add_trigger_only_during_business_hours_collect_to_campaigns.rb'
|
||||
- 'db/migrate/20211027073553_add_imap_smtp_config_to_channel_email.rb'
|
||||
- 'db/migrate/20211109143122_add_tweet_enabled_flag_to_twitter_channel.rb'
|
||||
- 'db/migrate/20211216110209_add_allow_messages_after_resolved_to_inbox.rb'
|
||||
- 'db/migrate/20220116103902_add_open_ssl_verify_mode_to_channel_email.rb'
|
||||
- 'db/migrate/20220216151613_add_open_all_day_to_working_hour.rb'
|
||||
- 'db/migrate/20220511072655_add_archive_column_to_portal.rb'
|
||||
- 'db/migrate/20230503101201_create_sla_policies.rb'
|
||||
RSpec/IndexedLet:
|
||||
Enabled: false
|
||||
RSpec/MatchArray:
|
||||
Enabled: false
|
||||
Rails/ResponseParsedBody:
|
||||
Enabled: false
|
||||
RSpec/FactoryBot/ConsistentParenthesesStyle:
|
||||
Enabled: false
|
||||
Rails/ThreeStateBooleanColumn:
|
||||
Enabled: false
|
||||
Rails/Pluck:
|
||||
Enabled: false
|
||||
Rails/TopLevelHashWithIndifferentAccess:
|
||||
Enabled: false
|
||||
Rails/ActionOrder:
|
||||
Enabled: false
|
||||
Style/ArrayIntersect:
|
||||
Enabled: false
|
||||
RSpec/NoExpectationExample:
|
||||
Enabled: false
|
||||
Style/RedundantReturn:
|
||||
Enabled: false
|
||||
Rails/I18nLocaleTexts:
|
||||
RSpec/NamedSubject:
|
||||
Enabled: false
|
||||
|
||||
|
||||
# we should bring this down
|
||||
RSpec/MultipleMemoizedHelpers:
|
||||
Max: 14
|
||||
|
||||
@@ -784,6 +784,7 @@ GEM
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-20
|
||||
arm64-darwin-22
|
||||
arm64-darwin-21
|
||||
ruby
|
||||
x86_64-darwin-18
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2.2.0
|
||||
2.17.0
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2.1.0
|
||||
2.3.0
|
||||
|
||||
@@ -16,6 +16,9 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
end
|
||||
|
||||
def show; end
|
||||
def edit; end
|
||||
|
||||
def create
|
||||
@article = @portal.articles.create!(article_params)
|
||||
@article.associate_root_article(article_params[:associated_article_id])
|
||||
@@ -23,10 +26,6 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
||||
render json: { error: @article.errors.messages }, status: :unprocessable_entity and return unless @article.valid?
|
||||
end
|
||||
|
||||
def edit; end
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
@article.update!(article_params) if params[:article].present?
|
||||
render json: { error: @article.errors.messages }, status: :unprocessable_entity and return unless @article.valid?
|
||||
|
||||
@@ -6,6 +6,8 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
||||
@automation_rules = Current.account.automation_rules
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@automation_rule = Current.account.automation_rules.new(automation_rules_permit)
|
||||
@automation_rule.actions = params[:actions]
|
||||
@@ -28,8 +30,6 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
||||
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
ActiveRecord::Base.transaction do
|
||||
automation_rule_update
|
||||
|
||||
@@ -6,21 +6,21 @@ class Api::V1::Accounts::CampaignsController < Api::V1::Accounts::BaseController
|
||||
@campaigns = Current.account.campaigns
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@campaign = Current.account.campaigns.create!(campaign_params)
|
||||
end
|
||||
|
||||
def update
|
||||
@campaign.update!(campaign_params)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@campaign.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
@campaign.update!(campaign_params)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def campaign
|
||||
|
||||
@@ -9,6 +9,8 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
|
||||
@categories = @portal.categories.search(params)
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@category = @portal.categories.create!(category_params)
|
||||
@category.related_categories << related_categories_records
|
||||
@@ -17,8 +19,6 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
|
||||
@category.save!
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
@category.update!(category_params)
|
||||
@category.related_categories = related_categories_records if related_categories_records.any?
|
||||
|
||||
@@ -5,21 +5,21 @@ class Api::V1::Accounts::Contacts::NotesController < Api::V1::Accounts::Contacts
|
||||
@notes = @contact.notes.latest.includes(:user)
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@note = @contact.notes.create!(note_params)
|
||||
end
|
||||
|
||||
def update
|
||||
@note.update(note_params)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@note.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
@note.update(note_params)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def note
|
||||
|
||||
@@ -26,6 +26,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
@attachments = @conversation.attachments
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
@conversation = ConversationBuilder.new(params: params, contact_inbox: @contact_inbox).perform
|
||||
@@ -33,8 +35,6 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
end
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def filter
|
||||
result = ::Conversations::FilterService.new(params.permit!, current_user).perform
|
||||
@conversations = result[:conversations]
|
||||
|
||||
@@ -34,9 +34,12 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base
|
||||
end
|
||||
|
||||
def set_csat_survey_responses
|
||||
@csat_survey_responses = filtrate(
|
||||
Current.account.csat_survey_responses.includes([:conversation, :assigned_agent, :contact])
|
||||
).filter_by_created_at(range).filter_by_assigned_agent_id(params[:user_ids])
|
||||
base_query = Current.account.csat_survey_responses.includes([:conversation, :assigned_agent, :contact])
|
||||
@csat_survey_responses = filtrate(base_query).filter_by_created_at(range)
|
||||
.filter_by_assigned_agent_id(params[:user_ids])
|
||||
.filter_by_inbox_id(params[:inbox_id])
|
||||
.filter_by_team_id(params[:team_id])
|
||||
.filter_by_rating(params[:rating])
|
||||
end
|
||||
|
||||
def set_current_page_surveys
|
||||
|
||||
@@ -2,6 +2,11 @@ class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseControl
|
||||
before_action :fetch_inbox
|
||||
before_action :current_agents_ids, only: [:create, :update]
|
||||
|
||||
def show
|
||||
authorize @inbox, :show?
|
||||
fetch_updated_agents
|
||||
end
|
||||
|
||||
def create
|
||||
authorize @inbox, :create?
|
||||
ActiveRecord::Base.transaction do
|
||||
@@ -10,11 +15,6 @@ class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseControl
|
||||
fetch_updated_agents
|
||||
end
|
||||
|
||||
def show
|
||||
authorize @inbox, :show?
|
||||
fetch_updated_agents
|
||||
end
|
||||
|
||||
def update
|
||||
authorize @inbox, :update?
|
||||
update_agents_list
|
||||
|
||||
@@ -6,6 +6,10 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
||||
@macros = Macro.with_visibility(current_user, params)
|
||||
end
|
||||
|
||||
def show
|
||||
head :not_found if @macro.nil?
|
||||
end
|
||||
|
||||
def create
|
||||
@macro = Current.account.macros.new(macros_with_user.merge(created_by_id: current_user.id))
|
||||
@macro.set_visibility(current_user, permitted_params)
|
||||
@@ -18,8 +22,16 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
||||
@macro
|
||||
end
|
||||
|
||||
def show
|
||||
head :not_found if @macro.nil?
|
||||
def update
|
||||
ActiveRecord::Base.transaction do
|
||||
@macro.update!(macros_with_user)
|
||||
@macro.set_visibility(current_user, permitted_params)
|
||||
process_attachments
|
||||
@macro.save!
|
||||
rescue StandardError => e
|
||||
Rails.logger.error e
|
||||
render json: { error: @macro.errors.messages }.to_json, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@@ -37,18 +49,6 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
||||
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
|
||||
end
|
||||
|
||||
def update
|
||||
ActiveRecord::Base.transaction do
|
||||
@macro.update!(macros_with_user)
|
||||
@macro.set_visibility(current_user, permitted_params)
|
||||
process_attachments
|
||||
@macro.save!
|
||||
rescue StandardError => e
|
||||
Rails.logger.error e
|
||||
render json: { error: @macro.errors.messages }.to_json, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def execute
|
||||
::MacrosExecutionJob.perform_later(@macro, conversation_ids: params[:conversation_ids], user: Current.user)
|
||||
|
||||
|
||||
@@ -14,6 +14,11 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
CustomExceptions::Account::UserErrors,
|
||||
with: :render_error_response
|
||||
|
||||
def show
|
||||
@latest_chatwoot_version = ::Redis::Alfred.get(::Redis::Alfred::LATEST_CHATWOOT_VERSION)
|
||||
render 'api/v1/accounts/show', format: :json
|
||||
end
|
||||
|
||||
def create
|
||||
@user, @account = AccountBuilder.new(
|
||||
account_name: account_params[:account_name],
|
||||
@@ -35,11 +40,6 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
render json: { cache_keys: get_cache_keys }, status: :ok
|
||||
end
|
||||
|
||||
def show
|
||||
@latest_chatwoot_version = ::Redis::Alfred.get(::Redis::Alfred::LATEST_CHATWOOT_VERSION)
|
||||
render 'api/v1/accounts/show', format: :json
|
||||
end
|
||||
|
||||
def update
|
||||
@account.update!(account_params.slice(:name, :locale, :domain, :support_email, :auto_resolve_duration))
|
||||
end
|
||||
|
||||
@@ -4,6 +4,16 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController
|
||||
skip_before_action :require_no_authentication, raise: false
|
||||
skip_before_action :authenticate_user!, raise: false
|
||||
|
||||
def create
|
||||
@user = User.find_by(email: params[:email])
|
||||
if @user
|
||||
@user.send_reset_password_instructions
|
||||
build_response(I18n.t('messages.reset_password_success'), 200)
|
||||
else
|
||||
build_response(I18n.t('messages.reset_password_failure'), 404)
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
# params: reset_password_token, password, password_confirmation
|
||||
original_token = params[:reset_password_token]
|
||||
@@ -17,16 +27,6 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
@user = User.find_by(email: params[:email])
|
||||
if @user
|
||||
@user.send_reset_password_instructions
|
||||
build_response(I18n.t('messages.reset_password_success'), 200)
|
||||
else
|
||||
build_response(I18n.t('messages.reset_password_failure'), 404)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reset_password_and_confirmation(recoverable)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
class DeviseOverrides::SessionsController < ::DeviseTokenAuth::SessionsController
|
||||
class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController
|
||||
# Prevent session parameter from being passed
|
||||
# Unpermitted parameter: session
|
||||
wrap_parameters format: []
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
class DeviseOverrides::TokenValidationsController < ::DeviseTokenAuth::TokenValidationsController
|
||||
class DeviseOverrides::TokenValidationsController < DeviseTokenAuth::TokenValidationsController
|
||||
def validate_token
|
||||
# @resource will have been set by set_user_by_token concern
|
||||
if @resource
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
class Platform::Api::V1::AccountsController < PlatformController
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@resource = Account.create!(account_params)
|
||||
update_resource_features
|
||||
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
@resource.assign_attributes(account_params)
|
||||
update_resource_features
|
||||
|
||||
@@ -6,14 +6,14 @@ class Platform::Api::V1::AgentBotsController < PlatformController
|
||||
@resources = @platform_app.platform_app_permissibles.where(permissible_type: 'AgentBot').all
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@resource = AgentBot.new(agent_bot_params)
|
||||
@resource.save!
|
||||
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
@resource.update!(agent_bot_params)
|
||||
end
|
||||
|
||||
@@ -5,6 +5,8 @@ class Platform::Api::V1::UsersController < PlatformController
|
||||
before_action(only: [:login]) { set_resource }
|
||||
before_action(only: [:login]) { validate_platform_app_permissible }
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@resource = (User.find_by(email: user_params[:email]) || User.new(user_params))
|
||||
@resource.skip_confirmation!
|
||||
@@ -16,8 +18,6 @@ class Platform::Api::V1::UsersController < PlatformController
|
||||
render json: { url: @resource.generate_sso_link }
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
@resource.assign_attributes(user_update_params)
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
|
||||
before_action :contact_inbox, except: [:create]
|
||||
before_action :process_hmac
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
source_id = params[:source_id] || SecureRandom.uuid
|
||||
@contact_inbox = ::ContactInboxWithContactBuilder.new(
|
||||
@@ -11,8 +13,6 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
|
||||
).perform
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
contact_identify_action = ContactIdentifyAction.new(
|
||||
contact: @contact_inbox.contact,
|
||||
|
||||
@@ -45,13 +45,17 @@ class SuperAdmin::AccountsController < SuperAdmin::ApplicationController
|
||||
|
||||
def seed
|
||||
Internal::SeedAccountJob.perform_later(requested_resource)
|
||||
# rubocop:disable Rails/I18nLocaleTexts
|
||||
redirect_back(fallback_location: [namespace, requested_resource], notice: 'Account seeding triggered')
|
||||
# rubocop:enable Rails/I18nLocaleTexts
|
||||
end
|
||||
|
||||
def destroy
|
||||
account = Account.find(params[:id])
|
||||
|
||||
DeleteObjectJob.perform_later(account) if account.present?
|
||||
# rubocop:disable Rails/I18nLocaleTexts
|
||||
redirect_back(fallback_location: [namespace, requested_resource], notice: 'Account deletion is in progress.')
|
||||
# rubocop:enable Rails/I18nLocaleTexts
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class SwaggerController < ApplicationController
|
||||
def respond
|
||||
if Rails.env.development? || Rails.env.test?
|
||||
render inline: File.read(Rails.root.join('swagger', derived_path))
|
||||
render inline: Rails.root.join('swagger', derived_path).read
|
||||
else
|
||||
head :not_found
|
||||
end
|
||||
|
||||
@@ -6,7 +6,7 @@ class CSATReportsAPI extends ApiClient {
|
||||
super('csat_survey_responses', { accountScoped: true });
|
||||
}
|
||||
|
||||
get({ page, from, to, user_ids } = {}) {
|
||||
get({ page, from, to, user_ids, inbox_id, team_id, rating } = {}) {
|
||||
return axios.get(this.url, {
|
||||
params: {
|
||||
page,
|
||||
@@ -14,24 +14,31 @@ class CSATReportsAPI extends ApiClient {
|
||||
until: to,
|
||||
sort: '-created_at',
|
||||
user_ids,
|
||||
inbox_id,
|
||||
team_id,
|
||||
rating,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
download({ from, to, user_ids } = {}) {
|
||||
download({ from, to, user_ids, inbox_id, team_id, rating } = {}) {
|
||||
return axios.get(`${this.url}/download`, {
|
||||
params: {
|
||||
since: from,
|
||||
until: to,
|
||||
sort: '-created_at',
|
||||
user_ids,
|
||||
inbox_id,
|
||||
team_id,
|
||||
rating,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getMetrics({ from, to, user_ids } = {}) {
|
||||
getMetrics({ from, to, user_ids, inbox_id, team_id } = {}) {
|
||||
// no ratings for metrics
|
||||
return axios.get(`${this.url}/metrics`, {
|
||||
params: { since: from, until: to, user_ids },
|
||||
params: { since: from, until: to, user_ids, inbox_id, team_id },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,20 @@
|
||||
}
|
||||
|
||||
.date-picker {
|
||||
.mx-datepicker {
|
||||
width: 100%;
|
||||
&.no-margin {
|
||||
.mx-input {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mx-datepicker-range {
|
||||
width: 320px;
|
||||
&:not(.auto-width) {
|
||||
.mx-datepicker-range {
|
||||
width: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx-datepicker {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mx-input {
|
||||
|
||||
@@ -261,6 +261,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Basic filter dropdown
|
||||
.basic-filter {
|
||||
left: 0;
|
||||
right: unset;
|
||||
}
|
||||
|
||||
// Card label
|
||||
.label-container {
|
||||
.label {
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
}
|
||||
|
||||
.multiselect {
|
||||
margin-bottom: var(--space-normal);
|
||||
&:not(.no-margin) {
|
||||
margin-bottom: var(--space-normal);
|
||||
}
|
||||
|
||||
&.multiselect--disabled {
|
||||
opacity: 0.8;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
}"
|
||||
@mouseenter="onCardHover"
|
||||
@mouseleave="onCardLeave"
|
||||
@click="cardClick(chat)"
|
||||
@click="onCardClick"
|
||||
@contextmenu="openContextMenu($event)"
|
||||
>
|
||||
<label v-if="hovered || selected" class="checkbox-wrapper" @click.stop>
|
||||
@@ -313,21 +313,33 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
cardClick(chat) {
|
||||
const { activeInbox } = this;
|
||||
const path = conversationUrl({
|
||||
accountId: this.accountId,
|
||||
activeInbox,
|
||||
id: chat.id,
|
||||
label: this.activeLabel,
|
||||
teamId: this.teamId,
|
||||
foldersId: this.foldersId,
|
||||
conversationType: this.conversationType,
|
||||
});
|
||||
onCardClick(e) {
|
||||
const { activeInbox, chat } = this;
|
||||
const path = frontendURL(
|
||||
conversationUrl({
|
||||
accountId: this.accountId,
|
||||
activeInbox,
|
||||
id: chat.id,
|
||||
label: this.activeLabel,
|
||||
teamId: this.teamId,
|
||||
foldersId: this.foldersId,
|
||||
conversationType: this.conversationType,
|
||||
})
|
||||
);
|
||||
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
window.open(
|
||||
window.chatwootConfig.hostURL + path,
|
||||
'_blank',
|
||||
'noopener noreferrer nofollow'
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (this.isActiveChat) {
|
||||
return;
|
||||
}
|
||||
router.push({ path: frontendURL(path) });
|
||||
|
||||
router.push({ path });
|
||||
},
|
||||
onCardHover() {
|
||||
this.hovered = !this.hideThumbnail;
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
{
|
||||
"CSAT": {
|
||||
"TITLE": "Rate your conversation",
|
||||
"PLACEHOLDER": "Tell us more..."
|
||||
"PLACEHOLDER": "Tell us more...",
|
||||
"RATINGS": {
|
||||
"POOR": "😞 Poor",
|
||||
"FAIR": "😑 Fair",
|
||||
"AVERAGE": "😐 Average",
|
||||
"GOOD": "😀 Good",
|
||||
"EXCELLENT": "😍 Excellent"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
"LOADING_CHART": "Loading chart data...",
|
||||
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
|
||||
"DOWNLOAD_AGENT_REPORTS": "Download agent reports",
|
||||
"DATA_FETCHING_FAILED": "Failed to fetch data, please try again later.",
|
||||
"SUMMARY_FETCHING_FAILED": "Failed to fetch summary, please try again later.",
|
||||
"METRICS": {
|
||||
"CONVERSATIONS": {
|
||||
"NAME": "Conversations",
|
||||
@@ -34,6 +36,14 @@
|
||||
"DESC": "( Total )"
|
||||
}
|
||||
},
|
||||
"DATE_RANGE_OPTIONS": {
|
||||
"LAST_7_DAYS": "Last 7 days",
|
||||
"LAST_30_DAYS": "Last 30 days",
|
||||
"LAST_3_MONTHS": "Last 3 months",
|
||||
"LAST_6_MONTHS": "Last 6 months",
|
||||
"LAST_YEAR": "Last year",
|
||||
"CUSTOM_DATE_RANGE": "Custom date range"
|
||||
},
|
||||
"DATE_RANGE": [
|
||||
{
|
||||
"id": 0,
|
||||
@@ -66,6 +76,12 @@
|
||||
},
|
||||
"GROUP_BY_FILTER_DROPDOWN_LABEL": "Group By",
|
||||
"DURATION_FILTER_LABEL": "Duration",
|
||||
"GROUPING_OPTIONS": {
|
||||
"DAY": "Day",
|
||||
"WEEK": "Week",
|
||||
"MONTH": "Month",
|
||||
"YEAR": "Year"
|
||||
},
|
||||
"GROUP_BY_DAY_OPTIONS": [{ "id": 1, "groupBy": "Day" }],
|
||||
"GROUP_BY_WEEK_OPTIONS": [
|
||||
{ "id": 1, "groupBy": "Day" },
|
||||
@@ -356,6 +372,7 @@
|
||||
"HEADER": "CSAT Reports",
|
||||
"NO_RECORDS": "There are no CSAT survey responses available.",
|
||||
"DOWNLOAD": "Download CSAT Reports",
|
||||
"DOWNLOAD_FAILED": "Failed to download CSAT Reports",
|
||||
"FILTERS": {
|
||||
"AGENTS": {
|
||||
"PLACEHOLDER": "Choose Agents"
|
||||
|
||||
+4
-1
@@ -37,7 +37,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<thumbnail
|
||||
v-if="notificationItem.primary_actor.meta.assignee"
|
||||
v-if="hasAssignee(notificationItem)"
|
||||
:src="notificationItem.primary_actor.meta.assignee.thumbnail"
|
||||
size="16px"
|
||||
:username="notificationItem.primary_actor.meta.assignee.name"
|
||||
@@ -127,6 +127,9 @@ export default {
|
||||
});
|
||||
}
|
||||
},
|
||||
hasAssignee(notification) {
|
||||
return notification.primary_actor.meta?.assignee;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<template>
|
||||
<div class="column content-box">
|
||||
<report-filter-selector
|
||||
agents-filter
|
||||
:agents-filter-items-list="agentList"
|
||||
:show-agents-filter="true"
|
||||
:show-inbox-filter="true"
|
||||
:show-rating-filter="true"
|
||||
:show-team-filter="isTeamsEnabled"
|
||||
:show-business-hours-switch="false"
|
||||
@date-range-change="onDateRangeChange"
|
||||
@agents-filter-change="onAgentsFilterChange"
|
||||
@filter-change="onFilterChange"
|
||||
/>
|
||||
<woot-button
|
||||
color-scheme="success"
|
||||
@@ -23,9 +24,11 @@
|
||||
import CsatMetrics from './components/CsatMetrics';
|
||||
import CsatTable from './components/CsatTable';
|
||||
import ReportFilterSelector from './components/FilterSelector';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { generateFileName } from '../../../../helper/downloadHelper';
|
||||
import { REPORTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { FEATURE_FLAGS } from '../../../../featureFlags';
|
||||
import alertMixin from '../../../../../shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
name: 'CsatResponses',
|
||||
@@ -34,39 +37,78 @@ export default {
|
||||
CsatTable,
|
||||
ReportFilterSelector,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
data() {
|
||||
return { pageIndex: 1, from: 0, to: 0, userIds: [] };
|
||||
return {
|
||||
pageIndex: 1,
|
||||
from: 0,
|
||||
to: 0,
|
||||
userIds: [],
|
||||
inbox: null,
|
||||
team: null,
|
||||
rating: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
agentList: 'agents/getAgents',
|
||||
accountId: 'getCurrentAccountId',
|
||||
isFeatureEnabledOnAccount: 'accounts/isFeatureEnabledonAccount',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('agents/get');
|
||||
},
|
||||
methods: {
|
||||
getAllData() {
|
||||
this.$store.dispatch('csat/getMetrics', {
|
||||
requestPayload() {
|
||||
return {
|
||||
from: this.from,
|
||||
to: this.to,
|
||||
user_ids: this.userIds,
|
||||
});
|
||||
this.getResponses();
|
||||
inbox_id: this.inbox,
|
||||
team_id: this.team,
|
||||
rating: this.rating,
|
||||
};
|
||||
},
|
||||
isTeamsEnabled() {
|
||||
return this.isFeatureEnabledOnAccount(
|
||||
this.accountId,
|
||||
FEATURE_FLAGS.TEAM_MANAGEMENT
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getAllData() {
|
||||
try {
|
||||
this.$store.dispatch('csat/getMetrics', this.requestPayload);
|
||||
this.getResponses();
|
||||
} catch {
|
||||
this.showAlert(this.$t('REPORT.DATA_FETCHING_FAILED'));
|
||||
}
|
||||
},
|
||||
getResponses() {
|
||||
this.$store.dispatch('csat/get', {
|
||||
page: this.pageIndex,
|
||||
from: this.from,
|
||||
to: this.to,
|
||||
user_ids: this.userIds,
|
||||
...this.requestPayload,
|
||||
});
|
||||
},
|
||||
downloadReports() {
|
||||
const type = 'csat';
|
||||
try {
|
||||
this.$store.dispatch('csat/downloadCSATReports', {
|
||||
fileName: generateFileName({ type, to: this.to }),
|
||||
...this.requestPayload,
|
||||
});
|
||||
} catch (error) {
|
||||
this.showAlert(this.$t('REPORT.CSAT_REPORTS.DOWNLOAD_FAILED'));
|
||||
}
|
||||
},
|
||||
onPageNumberChange(pageIndex) {
|
||||
this.pageIndex = pageIndex;
|
||||
this.getResponses();
|
||||
},
|
||||
onDateRangeChange({ from, to }) {
|
||||
onFilterChange({
|
||||
from,
|
||||
to,
|
||||
selectedAgents,
|
||||
selectedInbox,
|
||||
selectedTeam,
|
||||
selectedRating,
|
||||
}) {
|
||||
// do not track filter change on inital load
|
||||
if (this.from !== 0 && this.to !== 0) {
|
||||
this.$track(REPORTS_EVENTS.FILTER_REPORT, {
|
||||
@@ -74,27 +116,16 @@ export default {
|
||||
reportType: 'csat',
|
||||
});
|
||||
}
|
||||
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
this.userIds = selectedAgents.map(el => el.id);
|
||||
this.inbox = selectedInbox?.id;
|
||||
this.team = selectedTeam?.id;
|
||||
this.rating = selectedRating?.value;
|
||||
|
||||
this.getAllData();
|
||||
},
|
||||
onAgentsFilterChange(agents) {
|
||||
this.userIds = agents.map(el => el.id);
|
||||
this.getAllData();
|
||||
this.$track(REPORTS_EVENTS.FILTER_REPORT, {
|
||||
filterType: 'agent',
|
||||
reportType: 'csat',
|
||||
});
|
||||
},
|
||||
downloadReports() {
|
||||
const type = 'csat';
|
||||
this.$store.dispatch('csat/downloadCSATReports', {
|
||||
from: this.from,
|
||||
to: this.to,
|
||||
user_ids: this.userIds,
|
||||
fileName: generateFileName({ type, to: this.to }),
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -9,12 +9,9 @@
|
||||
{{ $t('REPORT.DOWNLOAD_AGENT_REPORTS') }}
|
||||
</woot-button>
|
||||
<report-filter-selector
|
||||
group-by-filter
|
||||
:selected-group-by-filter="selectedGroupByFilter"
|
||||
:filter-items-list="filterItemsList"
|
||||
@date-range-change="onDateRangeChange"
|
||||
:show-agents-filter="false"
|
||||
:show-group-by-filter="true"
|
||||
@filter-change="onFilterChange"
|
||||
@business-hours-toggle="onBusinessHoursToggle"
|
||||
/>
|
||||
<div class="row">
|
||||
<woot-report-stats-card
|
||||
@@ -55,7 +52,8 @@ import fromUnixTime from 'date-fns/fromUnixTime';
|
||||
import format from 'date-fns/format';
|
||||
import ReportFilterSelector from './components/FilterSelector';
|
||||
import { GROUP_BY_FILTER, METRIC_CHART } from './constants';
|
||||
import reportMixin from '../../../../mixins/reportMixin';
|
||||
import reportMixin from 'dashboard/mixins/reportMixin';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { formatTime } from '@chatwoot/utils';
|
||||
import { REPORTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
|
||||
@@ -73,15 +71,13 @@ export default {
|
||||
components: {
|
||||
ReportFilterSelector,
|
||||
},
|
||||
mixins: [reportMixin],
|
||||
mixins: [reportMixin, alertMixin],
|
||||
data() {
|
||||
return {
|
||||
from: 0,
|
||||
to: 0,
|
||||
currentSelection: 0,
|
||||
groupBy: GROUP_BY_FILTER[1],
|
||||
filterItemsList: this.$t('REPORT.GROUP_BY_DAY_OPTIONS'),
|
||||
selectedGroupByFilter: {},
|
||||
businessHours: false,
|
||||
};
|
||||
},
|
||||
@@ -191,24 +187,35 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
fetchAllData() {
|
||||
const { from, to, groupBy, businessHours } = this;
|
||||
this.$store.dispatch('fetchAccountSummary', {
|
||||
from,
|
||||
to,
|
||||
groupBy: groupBy.period,
|
||||
businessHours,
|
||||
});
|
||||
this.fetchAccountSummary();
|
||||
this.fetchChartData();
|
||||
},
|
||||
fetchAccountSummary() {
|
||||
try {
|
||||
this.$store.dispatch('fetchAccountSummary', this.getRequestPayload());
|
||||
} catch {
|
||||
this.showAlert(this.$t('REPORT.SUMMARY_FETCHING_FAILED'));
|
||||
}
|
||||
},
|
||||
fetchChartData() {
|
||||
try {
|
||||
this.$store.dispatch('fetchAccountReport', {
|
||||
metric: this.metrics[this.currentSelection].KEY,
|
||||
...this.getRequestPayload(),
|
||||
});
|
||||
} catch {
|
||||
this.showAlert(this.$t('REPORT.DATA_FETCHING_FAILED'));
|
||||
}
|
||||
},
|
||||
getRequestPayload() {
|
||||
const { from, to, groupBy, businessHours } = this;
|
||||
this.$store.dispatch('fetchAccountReport', {
|
||||
metric: this.metrics[this.currentSelection].KEY,
|
||||
|
||||
return {
|
||||
from,
|
||||
to,
|
||||
groupBy: groupBy.period,
|
||||
businessHours,
|
||||
});
|
||||
};
|
||||
},
|
||||
downloadAgentReports() {
|
||||
const { from, to } = this;
|
||||
@@ -222,57 +229,15 @@ export default {
|
||||
this.currentSelection = index;
|
||||
this.fetchChartData();
|
||||
},
|
||||
onDateRangeChange({ from, to, groupBy }) {
|
||||
// do not track filter change on inital load
|
||||
if (this.from !== 0 && this.to !== 0) {
|
||||
this.$track(REPORTS_EVENTS.FILTER_REPORT, {
|
||||
filterType: 'date',
|
||||
reportType: 'conversations',
|
||||
});
|
||||
}
|
||||
onFilterChange({ from, to, groupBy, businessHours }) {
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
this.filterItemsList = this.fetchFilterItems(groupBy);
|
||||
const filterItems = this.filterItemsList.filter(
|
||||
item => item.id === this.groupBy.id
|
||||
);
|
||||
if (filterItems.length > 0) {
|
||||
this.selectedGroupByFilter = filterItems[0];
|
||||
} else {
|
||||
this.selectedGroupByFilter = this.filterItemsList[0];
|
||||
this.groupBy = GROUP_BY_FILTER[this.selectedGroupByFilter.id];
|
||||
}
|
||||
this.fetchAllData();
|
||||
},
|
||||
onFilterChange(payload) {
|
||||
this.groupBy = GROUP_BY_FILTER[payload.id];
|
||||
this.groupBy = groupBy;
|
||||
this.businessHours = businessHours;
|
||||
this.fetchAllData();
|
||||
|
||||
this.$track(REPORTS_EVENTS.FILTER_REPORT, {
|
||||
filterType: 'groupBy',
|
||||
filterValue: this.groupBy?.period,
|
||||
reportType: 'conversations',
|
||||
});
|
||||
},
|
||||
fetchFilterItems(groupBy) {
|
||||
switch (groupBy) {
|
||||
case GROUP_BY_FILTER[2].period:
|
||||
return this.$t('REPORT.GROUP_BY_WEEK_OPTIONS');
|
||||
case GROUP_BY_FILTER[3].period:
|
||||
return this.$t('REPORT.GROUP_BY_MONTH_OPTIONS');
|
||||
case GROUP_BY_FILTER[4].period:
|
||||
return this.$t('REPORT.GROUP_BY_YEAR_OPTIONS');
|
||||
default:
|
||||
return this.$t('REPORT.GROUP_BY_DAY_OPTIONS');
|
||||
}
|
||||
},
|
||||
onBusinessHoursToggle(value) {
|
||||
this.businessHours = value;
|
||||
this.fetchAllData();
|
||||
|
||||
this.$track(REPORTS_EVENTS.FILTER_REPORT, {
|
||||
filterType: 'businessHours',
|
||||
filterValue: value,
|
||||
filterValue: { from, to, groupBy, businessHours },
|
||||
reportType: 'conversations',
|
||||
});
|
||||
},
|
||||
|
||||
+162
-134
@@ -1,72 +1,43 @@
|
||||
<template>
|
||||
<div class="flex-container flex-dir-column medium-flex-dir-row">
|
||||
<div class="small-12 medium-3 pull-right multiselect-wrap--small">
|
||||
<multiselect
|
||||
v-model="currentDateRangeSelection"
|
||||
track-by="name"
|
||||
label="name"
|
||||
:placeholder="$t('FORMS.MULTISELECT.SELECT_ONE')"
|
||||
selected-label
|
||||
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||
deselect-label=""
|
||||
:options="dateRange"
|
||||
:searchable="false"
|
||||
:allow-empty="false"
|
||||
@select="changeDateSelection"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-container">
|
||||
<reports-filters-date-range @on-range-change="onDateRangeChange" />
|
||||
<woot-date-range-picker
|
||||
v-if="isDateRangeSelected"
|
||||
class="margin-left-1"
|
||||
show-range
|
||||
class="no-margin auto-width"
|
||||
:value="customDateRange"
|
||||
:confirm-text="$t('REPORT.CUSTOM_DATE_RANGE.CONFIRM')"
|
||||
:placeholder="$t('REPORT.CUSTOM_DATE_RANGE.PLACEHOLDER')"
|
||||
@change="onChange"
|
||||
@change="onCustomDateRangeChange"
|
||||
/>
|
||||
<div
|
||||
v-if="notLast7Days && groupByFilter"
|
||||
class="small-12 medium-3 pull-right margin-left-1 margin-right-1 multiselect-wrap--small"
|
||||
>
|
||||
<p aria-hidden="true" class="hide">
|
||||
{{ $t('REPORT.GROUP_BY_FILTER_DROPDOWN_LABEL') }}
|
||||
</p>
|
||||
<multiselect
|
||||
v-model="currentSelectedFilter"
|
||||
track-by="id"
|
||||
label="groupBy"
|
||||
:placeholder="$t('REPORT.GROUP_BY_FILTER_DROPDOWN_LABEL')"
|
||||
:options="filterItemsList"
|
||||
:allow-empty="false"
|
||||
:show-labels="false"
|
||||
@input="changeFilterSelection"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="agentsFilter"
|
||||
class="small-12 medium-3 pull-right margin-left-1 margin-right-1 multiselect-wrap--small"
|
||||
>
|
||||
<multiselect
|
||||
v-model="selectedAgents"
|
||||
:options="agentsFilterItemsList"
|
||||
track-by="id"
|
||||
label="name"
|
||||
:multiple="true"
|
||||
:close-on-select="false"
|
||||
:clear-on-select="false"
|
||||
:hide-selected="true"
|
||||
:placeholder="$t('CSAT_REPORTS.FILTERS.AGENTS.PLACEHOLDER')"
|
||||
selected-label
|
||||
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||
:deselect-label="$t('FORMS.MULTISELECT.ENTER_TO_REMOVE')"
|
||||
@input="handleAgentsFilterSelection"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="showBusinessHoursSwitch"
|
||||
class="small-12 medium-3 business-hours"
|
||||
>
|
||||
<span class="business-hours-text margin-right-1">
|
||||
<reports-filters-date-group-by
|
||||
v-if="showGroupByFilter && isGroupByPossible"
|
||||
:valid-group-options="validGroupOptions"
|
||||
:selected-option="selectedGroupByFilter"
|
||||
@on-grouping-change="onGroupingChange"
|
||||
/>
|
||||
<reports-filters-agents
|
||||
v-if="showAgentsFilter"
|
||||
@agents-filter-selection="handleAgentsFilterSelection"
|
||||
/>
|
||||
<reports-filters-labels
|
||||
v-if="showLabelsFilter"
|
||||
@labels-filter-selection="handleLabelsFilterSelection"
|
||||
/>
|
||||
<reports-filters-teams
|
||||
v-if="showTeamFilter"
|
||||
@team-filter-selection="handleTeamFilterSelection"
|
||||
/>
|
||||
<reports-filters-inboxes
|
||||
v-if="showInboxFilter"
|
||||
@inbox-filter-selection="handleInboxFilterSelection"
|
||||
/>
|
||||
<reports-filters-ratings
|
||||
v-if="showRatingFilter"
|
||||
@rating-filter-selection="handleRatingFilterSelection"
|
||||
/>
|
||||
<div v-if="showBusinessHoursSwitch" class="business-hours">
|
||||
<span class="business-hours-text ">
|
||||
{{ $t('REPORT.BUSINESS_HOURS') }}
|
||||
</span>
|
||||
<span>
|
||||
@@ -77,36 +48,54 @@
|
||||
</template>
|
||||
<script>
|
||||
import WootDateRangePicker from 'dashboard/components/ui/DateRangePicker.vue';
|
||||
import ReportsFiltersDateRange from './Filters/DateRange.vue';
|
||||
import ReportsFiltersDateGroupBy from './Filters/DateGroupBy.vue';
|
||||
import ReportsFiltersAgents from './Filters/Agents.vue';
|
||||
import ReportsFiltersLabels from './Filters/Labels.vue';
|
||||
import ReportsFiltersInboxes from './Filters/Inboxes.vue';
|
||||
import ReportsFiltersTeams from './Filters/Teams.vue';
|
||||
import ReportsFiltersRatings from './Filters/Ratings.vue';
|
||||
import subDays from 'date-fns/subDays';
|
||||
import startOfDay from 'date-fns/startOfDay';
|
||||
import getUnixTime from 'date-fns/getUnixTime';
|
||||
import { GROUP_BY_FILTER } from '../constants';
|
||||
import endOfDay from 'date-fns/endOfDay';
|
||||
|
||||
const CUSTOM_DATE_RANGE_ID = 5;
|
||||
import { DATE_RANGE_OPTIONS } from '../constants';
|
||||
import { getUnixStartOfDay, getUnixEndOfDay } from 'helpers/DateHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootDateRangePicker,
|
||||
ReportsFiltersDateRange,
|
||||
ReportsFiltersDateGroupBy,
|
||||
ReportsFiltersAgents,
|
||||
ReportsFiltersLabels,
|
||||
ReportsFiltersInboxes,
|
||||
ReportsFiltersTeams,
|
||||
ReportsFiltersRatings,
|
||||
},
|
||||
props: {
|
||||
filterItemsList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
agentsFilterItemsList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
selectedGroupByFilter: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
groupByFilter: {
|
||||
showGroupByFilter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
agentsFilter: {
|
||||
showAgentsFilter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showLabelsFilter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showInboxFilter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showRatingFilter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showTeamFilter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
@@ -117,95 +106,134 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentDateRangeSelection: this.$t('REPORT.DATE_RANGE')[0],
|
||||
dateRange: this.$t('REPORT.DATE_RANGE'),
|
||||
customDateRange: [new Date(), new Date()],
|
||||
currentSelectedFilter: null,
|
||||
// default value, need not be translated
|
||||
selectedDateRange: DATE_RANGE_OPTIONS.LAST_7_DAYS,
|
||||
selectedGroupByFilter: null,
|
||||
selectedLabel: null,
|
||||
selectedInbox: null,
|
||||
selectedTeam: null,
|
||||
selectedRating: null,
|
||||
selectedAgents: [],
|
||||
customDateRange: [new Date(), new Date()],
|
||||
businessHoursSelected: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isDateRangeSelected() {
|
||||
return this.currentDateRangeSelection.id === CUSTOM_DATE_RANGE_ID;
|
||||
return (
|
||||
this.selectedDateRange.id === DATE_RANGE_OPTIONS.CUSTOM_DATE_RANGE.id
|
||||
);
|
||||
},
|
||||
isGroupByPossible() {
|
||||
return this.selectedDateRange.id !== DATE_RANGE_OPTIONS.LAST_7_DAYS.id;
|
||||
},
|
||||
to() {
|
||||
if (this.isDateRangeSelected) {
|
||||
return this.toCustomDate(this.customDateRange[1]);
|
||||
return getUnixEndOfDay(this.customDateRange[1]);
|
||||
}
|
||||
return this.toCustomDate(new Date());
|
||||
return getUnixEndOfDay(new Date());
|
||||
},
|
||||
from() {
|
||||
if (this.isDateRangeSelected) {
|
||||
return this.fromCustomDate(this.customDateRange[0]);
|
||||
return getUnixStartOfDay(this.customDateRange[0]);
|
||||
}
|
||||
const dateRange = {
|
||||
0: 6,
|
||||
1: 29,
|
||||
2: 89,
|
||||
3: 179,
|
||||
4: 364,
|
||||
};
|
||||
const diff = dateRange[this.currentDateRangeSelection.id];
|
||||
const fromDate = subDays(new Date(), diff);
|
||||
return this.fromCustomDate(fromDate);
|
||||
|
||||
const { offset } = this.selectedDateRange;
|
||||
const fromDate = subDays(new Date(), offset);
|
||||
return getUnixStartOfDay(fromDate);
|
||||
},
|
||||
groupBy() {
|
||||
if (this.isDateRangeSelected) {
|
||||
return GROUP_BY_FILTER[4].period;
|
||||
validGroupOptions() {
|
||||
return this.selectedDateRange.groupByOptions;
|
||||
},
|
||||
validGroupBy() {
|
||||
if (!this.selectedGroupByFilter) {
|
||||
return this.validGroupOptions[0];
|
||||
}
|
||||
const groupRange = {
|
||||
0: GROUP_BY_FILTER[1].period,
|
||||
1: GROUP_BY_FILTER[2].period,
|
||||
2: GROUP_BY_FILTER[3].period,
|
||||
3: GROUP_BY_FILTER[3].period,
|
||||
4: GROUP_BY_FILTER[3].period,
|
||||
};
|
||||
return groupRange[this.currentDateRangeSelection.id];
|
||||
},
|
||||
notLast7Days() {
|
||||
return this.groupBy !== GROUP_BY_FILTER[1].period;
|
||||
|
||||
const validIds = this.validGroupOptions.map(opt => opt.id);
|
||||
if (validIds.includes(this.selectedGroupByFilter.id)) {
|
||||
return this.selectedGroupByFilter;
|
||||
}
|
||||
return this.validGroupOptions[0];
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
filterItemsList() {
|
||||
this.currentSelectedFilter = this.selectedGroupByFilter;
|
||||
},
|
||||
businessHoursSelected() {
|
||||
this.$emit('business-hours-toggle', this.businessHoursSelected);
|
||||
this.emitChange();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.onDateRangeChange();
|
||||
this.emitChange();
|
||||
},
|
||||
methods: {
|
||||
onDateRangeChange() {
|
||||
this.$emit('date-range-change', {
|
||||
from: this.from,
|
||||
to: this.to,
|
||||
groupBy: this.groupBy,
|
||||
emitChange() {
|
||||
const {
|
||||
from,
|
||||
to,
|
||||
selectedGroupByFilter: groupBy,
|
||||
businessHoursSelected: businessHours,
|
||||
selectedAgents,
|
||||
selectedLabel,
|
||||
selectedInbox,
|
||||
selectedTeam,
|
||||
selectedRating,
|
||||
} = this;
|
||||
this.$emit('filter-change', {
|
||||
from,
|
||||
to,
|
||||
groupBy,
|
||||
businessHours,
|
||||
selectedAgents,
|
||||
selectedLabel,
|
||||
selectedInbox,
|
||||
selectedTeam,
|
||||
selectedRating,
|
||||
});
|
||||
},
|
||||
fromCustomDate(date) {
|
||||
return getUnixTime(startOfDay(date));
|
||||
onDateRangeChange(selectedRange) {
|
||||
this.selectedDateRange = selectedRange;
|
||||
this.selectedGroupByFilter = this.validGroupBy;
|
||||
this.emitChange();
|
||||
},
|
||||
toCustomDate(date) {
|
||||
return getUnixTime(endOfDay(date));
|
||||
},
|
||||
changeDateSelection(selectedRange) {
|
||||
this.currentDateRangeSelection = selectedRange;
|
||||
this.onDateRangeChange();
|
||||
},
|
||||
onChange(value) {
|
||||
onCustomDateRangeChange(value) {
|
||||
this.customDateRange = value;
|
||||
this.onDateRangeChange();
|
||||
this.selectedGroupByFilter = this.validGroupBy;
|
||||
this.emitChange();
|
||||
},
|
||||
changeFilterSelection() {
|
||||
this.$emit('filter-change', this.currentSelectedFilter);
|
||||
onGroupingChange(payload) {
|
||||
this.selectedGroupByFilter = payload;
|
||||
this.emitChange();
|
||||
},
|
||||
handleAgentsFilterSelection() {
|
||||
this.$emit('agents-filter-change', this.selectedAgents);
|
||||
handleAgentsFilterSelection(selectedAgents) {
|
||||
this.selectedAgents = selectedAgents;
|
||||
this.emitChange();
|
||||
},
|
||||
handleLabelsFilterSelection(selectedLabel) {
|
||||
this.selectedLabel = selectedLabel;
|
||||
this.emitChange();
|
||||
},
|
||||
handleInboxFilterSelection(selectedInbox) {
|
||||
this.selectedInbox = selectedInbox;
|
||||
this.emitChange();
|
||||
},
|
||||
handleTeamFilterSelection(selectedTeam) {
|
||||
this.selectedTeam = selectedTeam;
|
||||
this.emitChange();
|
||||
},
|
||||
handleRatingFilterSelection(selectedRating) {
|
||||
this.selectedRating = selectedRating;
|
||||
this.emitChange();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filter-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
grid-gap: var(--space-slab);
|
||||
|
||||
margin-bottom: var(--space-normal);
|
||||
}
|
||||
</style>
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="multiselect-wrap--small">
|
||||
<multiselect
|
||||
v-model="selectedOptions"
|
||||
class="no-margin"
|
||||
:options="options"
|
||||
track-by="id"
|
||||
label="name"
|
||||
:multiple="true"
|
||||
:close-on-select="false"
|
||||
:clear-on-select="false"
|
||||
:hide-selected="true"
|
||||
:placeholder="$t('CSAT_REPORTS.FILTERS.AGENTS.PLACEHOLDER')"
|
||||
selected-label
|
||||
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||
:deselect-label="$t('FORMS.MULTISELECT.ENTER_TO_REMOVE')"
|
||||
@input="handleInput"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'ReportsFiltersAgents',
|
||||
data() {
|
||||
return {
|
||||
selectedOptions: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
options: 'agents/getAgents',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('agents/get');
|
||||
},
|
||||
methods: {
|
||||
handleInput() {
|
||||
this.$emit('agents-filter-selection', this.selectedOptions);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="multiselect-wrap--small">
|
||||
<p aria-hidden="true" class="hide">
|
||||
{{ $t('REPORT.GROUP_BY_FILTER_DROPDOWN_LABEL') }}
|
||||
</p>
|
||||
<multiselect
|
||||
v-model="currentSelectedFilter"
|
||||
class="no-margin"
|
||||
track-by="id"
|
||||
label="groupBy"
|
||||
:placeholder="$t('REPORT.GROUP_BY_FILTER_DROPDOWN_LABEL')"
|
||||
:options="translatedOptions"
|
||||
:allow-empty="false"
|
||||
:show-labels="false"
|
||||
@select="changeFilterSelection"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { GROUP_BY_OPTIONS } from '../../constants';
|
||||
|
||||
const EVENT_NAME = 'on-grouping-change';
|
||||
|
||||
export default {
|
||||
name: 'ReportsFiltersDateGroupBy',
|
||||
props: {
|
||||
validGroupOptions: {
|
||||
type: Array,
|
||||
default: () => [GROUP_BY_OPTIONS.DAY],
|
||||
},
|
||||
selectedOption: {
|
||||
type: Object,
|
||||
default: () => GROUP_BY_OPTIONS.DAY,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentSelectedFilter: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
translatedOptions() {
|
||||
return this.validGroupOptions.map(option => ({
|
||||
...option,
|
||||
groupBy: this.$t(option.translationKey),
|
||||
}));
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selectedOption: {
|
||||
handler() {
|
||||
this.currentSelectedFilter = {
|
||||
...this.selectedOption,
|
||||
groupBy: this.$t(this.selectedOption.translationKey),
|
||||
};
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
changeFilterSelection(selectedFilter) {
|
||||
this.groupByOptions = this.$emit(EVENT_NAME, selectedFilter);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="multiselect-wrap--small">
|
||||
<multiselect
|
||||
v-model="selectedOption"
|
||||
class="no-margin"
|
||||
track-by="name"
|
||||
label="name"
|
||||
:placeholder="$t('FORMS.MULTISELECT.SELECT_ONE')"
|
||||
selected-label
|
||||
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||
deselect-label=""
|
||||
:options="options"
|
||||
:searchable="false"
|
||||
:allow-empty="false"
|
||||
@select="updateRange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DATE_RANGE_OPTIONS } from '../../constants';
|
||||
|
||||
const EVENT_NAME = 'on-range-change';
|
||||
|
||||
export default {
|
||||
name: 'ReportFiltersDateRange',
|
||||
data() {
|
||||
const translatedOptions = Object.values(DATE_RANGE_OPTIONS).map(option => ({
|
||||
...option,
|
||||
name: this.$t(option.translationKey),
|
||||
}));
|
||||
|
||||
return {
|
||||
// relies on translations, need to move it to constants
|
||||
selectedOption: translatedOptions[0],
|
||||
options: translatedOptions,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
updateRange(selectedRange) {
|
||||
this.selectedOption = selectedRange;
|
||||
this.$emit(EVENT_NAME, selectedRange);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="multiselect-wrap--small">
|
||||
<multiselect
|
||||
v-model="selectedOption"
|
||||
class="no-margin"
|
||||
:placeholder="$t('INBOX_REPORTS.FILTER_DROPDOWN_LABEL')"
|
||||
label="name"
|
||||
track-by="id"
|
||||
:options="options"
|
||||
:option-height="24"
|
||||
:show-labels="false"
|
||||
@input="handleInput"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'ReportsFiltersInboxes',
|
||||
data() {
|
||||
return {
|
||||
selectedOption: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
options: 'inboxes/getInboxes',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('inboxes/get');
|
||||
},
|
||||
methods: {
|
||||
handleInput() {
|
||||
this.$emit('inbox-filter-selection', this.selectedOption);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div class="multiselect-wrap--small">
|
||||
<multiselect
|
||||
v-model="selectedOption"
|
||||
class="no-margin"
|
||||
:placeholder="$t('LABEL_REPORTS.FILTER_DROPDOWN_LABEL')"
|
||||
label="title"
|
||||
track-by="id"
|
||||
:options="options"
|
||||
:option-height="24"
|
||||
:show-labels="false"
|
||||
@input="handleInput"
|
||||
>
|
||||
<template slot="singleLabel" slot-scope="props">
|
||||
<div class="reports-option__wrap">
|
||||
<div
|
||||
:style="{ backgroundColor: props.option.color }"
|
||||
class="reports-option__rounded--item"
|
||||
/>
|
||||
<span class="reports-option__desc">
|
||||
<span class="reports-option__title">
|
||||
{{ props.option.title }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template slot="option" slot-scope="props">
|
||||
<div class="reports-option__wrap">
|
||||
<div
|
||||
:style="{ backgroundColor: props.option.color }"
|
||||
class="
|
||||
reports-option__rounded--item
|
||||
reports-option__item
|
||||
reports-option__label--swatch
|
||||
"
|
||||
/>
|
||||
<span class="reports-option__desc">
|
||||
<span class="reports-option__title">
|
||||
{{ props.option.title }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</multiselect>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'ReportsFiltersLabels',
|
||||
data() {
|
||||
return {
|
||||
selectedOption: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
options: 'labels/getLabels',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('labels/get');
|
||||
},
|
||||
methods: {
|
||||
handleInput() {
|
||||
this.$emit('labels-filter-selection', this.selectedOption);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="multiselect-wrap--small">
|
||||
<multiselect
|
||||
v-model="selectedOption"
|
||||
class="no-margin"
|
||||
:option-height="24"
|
||||
:placeholder="$t('FORMS.MULTISELECT.SELECT_ONE')"
|
||||
:options="options"
|
||||
:show-labels="false"
|
||||
track-by="value"
|
||||
label="label"
|
||||
@input="handleInput"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { CSAT_RATINGS } from 'shared/constants/messages';
|
||||
|
||||
export default {
|
||||
name: 'ReportFiltersRatings',
|
||||
data() {
|
||||
const translatedOptions = CSAT_RATINGS.reverse().map(option => ({
|
||||
...option,
|
||||
label: this.$t(option.translationKey),
|
||||
}));
|
||||
|
||||
return {
|
||||
selectedOption: null,
|
||||
options: translatedOptions,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleInput(selectedRating) {
|
||||
this.$emit('rating-filter-selection', selectedRating);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="multiselect-wrap--small">
|
||||
<multiselect
|
||||
v-model="selectedOption"
|
||||
class="no-margin"
|
||||
:placeholder="$t('TEAM_REPORTS.FILTER_DROPDOWN_LABEL')"
|
||||
label="name"
|
||||
track-by="id"
|
||||
:options="options"
|
||||
:option-height="24"
|
||||
:show-labels="false"
|
||||
@input="handleInput"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'ReportsFiltersTeams',
|
||||
data() {
|
||||
return {
|
||||
selectedOption: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
options: 'teams/getTeams',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('teams/get');
|
||||
},
|
||||
methods: {
|
||||
handleInput() {
|
||||
this.$emit('team-filter-selection', this.selectedOption);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import ReportsFiltersAgents from '../../Filters/Agents';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
const mockStore = new Vuex.Store({
|
||||
modules: {
|
||||
agents: {
|
||||
namespaced: true,
|
||||
state: {
|
||||
agents: [],
|
||||
},
|
||||
getters: {
|
||||
getAgents: state => state.agents,
|
||||
},
|
||||
actions: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const mountParams = {
|
||||
localVue,
|
||||
store: mockStore,
|
||||
mocks: {
|
||||
$t: msg => msg,
|
||||
},
|
||||
stubs: ['multiselect'],
|
||||
};
|
||||
|
||||
describe('ReportsFiltersAgents.vue', () => {
|
||||
it('emits "agents-filter-selection" event when handleInput is called', () => {
|
||||
const wrapper = shallowMount(ReportsFiltersAgents, mountParams);
|
||||
|
||||
const selectedAgents = [
|
||||
{ id: 1, name: 'Agent 1' },
|
||||
{ id: 2, name: 'Agent 2' },
|
||||
];
|
||||
wrapper.setData({ selectedOptions: selectedAgents });
|
||||
|
||||
wrapper.vm.handleInput();
|
||||
|
||||
expect(wrapper.emitted('agents-filter-selection')).toBeTruthy();
|
||||
expect(wrapper.emitted('agents-filter-selection')[0]).toEqual([
|
||||
selectedAgents,
|
||||
]);
|
||||
});
|
||||
|
||||
it('dispatches the "agents/get" action when the component is mounted', () => {
|
||||
const dispatchSpy = jest.spyOn(mockStore, 'dispatch');
|
||||
|
||||
shallowMount(ReportsFiltersAgents, mountParams);
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith('agents/get');
|
||||
});
|
||||
});
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import ReportsFiltersDateGroupBy from '../../Filters/DateGroupBy';
|
||||
import { GROUP_BY_OPTIONS } from '../../../constants';
|
||||
|
||||
const mountParams = {
|
||||
mocks: {
|
||||
$t: msg => msg,
|
||||
},
|
||||
stubs: ['multiselect'],
|
||||
};
|
||||
|
||||
describe('ReportsFiltersDateGroupBy.vue', () => {
|
||||
it('emits "on-grouping-change" event when changeFilterSelection is called', () => {
|
||||
const wrapper = shallowMount(ReportsFiltersDateGroupBy, mountParams);
|
||||
|
||||
const selectedFilter = GROUP_BY_OPTIONS.DAY;
|
||||
wrapper.vm.changeFilterSelection(selectedFilter);
|
||||
|
||||
expect(wrapper.emitted('on-grouping-change')).toBeTruthy();
|
||||
expect(wrapper.emitted('on-grouping-change')[0]).toEqual([selectedFilter]);
|
||||
});
|
||||
|
||||
it('updates currentSelectedFilter when selectedOption is changed', async () => {
|
||||
const wrapper = shallowMount(ReportsFiltersDateGroupBy, mountParams);
|
||||
|
||||
const newSelectedOption = GROUP_BY_OPTIONS.MONTH;
|
||||
await wrapper.setProps({ selectedOption: newSelectedOption });
|
||||
|
||||
expect(wrapper.vm.currentSelectedFilter).toEqual({
|
||||
...newSelectedOption,
|
||||
groupBy: newSelectedOption.translationKey,
|
||||
});
|
||||
});
|
||||
|
||||
it('initializes translatedOptions correctly', () => {
|
||||
const wrapper = shallowMount(ReportsFiltersDateGroupBy, mountParams);
|
||||
|
||||
const expectedOptions = wrapper.vm.validGroupOptions.map(option => ({
|
||||
...option,
|
||||
groupBy: option.translationKey,
|
||||
}));
|
||||
|
||||
expect(wrapper.vm.translatedOptions).toEqual(expectedOptions);
|
||||
});
|
||||
});
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import ReportFiltersDateRange from '../../Filters/DateRange';
|
||||
import { DATE_RANGE_OPTIONS } from '../../../constants';
|
||||
|
||||
const mountParams = {
|
||||
mocks: {
|
||||
$t: msg => msg,
|
||||
},
|
||||
stubs: ['multiselect'],
|
||||
};
|
||||
|
||||
describe('ReportFiltersDateRange.vue', () => {
|
||||
it('emits "on-range-change" event when updateRange is called', () => {
|
||||
const wrapper = shallowMount(ReportFiltersDateRange, mountParams);
|
||||
|
||||
const selectedRange = DATE_RANGE_OPTIONS.LAST_7_DAYS;
|
||||
wrapper.vm.updateRange(selectedRange);
|
||||
|
||||
expect(wrapper.emitted('on-range-change')).toBeTruthy();
|
||||
expect(wrapper.emitted('on-range-change')[0]).toEqual([selectedRange]);
|
||||
});
|
||||
|
||||
it('initializes options correctly', () => {
|
||||
const wrapper = shallowMount(ReportFiltersDateRange, mountParams);
|
||||
|
||||
const expectedOptions = Object.values(DATE_RANGE_OPTIONS).map(option => ({
|
||||
...option,
|
||||
name: option.translationKey,
|
||||
}));
|
||||
|
||||
expect(wrapper.vm.options).toEqual(expectedOptions);
|
||||
});
|
||||
|
||||
it('initializes selectedOption correctly', () => {
|
||||
const wrapper = shallowMount(ReportFiltersDateRange, mountParams);
|
||||
const expectedSelectedOption = Object.values(DATE_RANGE_OPTIONS)[0];
|
||||
expect(wrapper.vm.selectedOption).toEqual({
|
||||
...expectedSelectedOption,
|
||||
name: expectedSelectedOption.translationKey,
|
||||
});
|
||||
});
|
||||
});
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import ReportsFiltersInboxes from '../../Filters/Inboxes';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
const mountParams = {
|
||||
mocks: {
|
||||
$t: msg => msg,
|
||||
},
|
||||
stubs: ['multiselect'],
|
||||
};
|
||||
|
||||
describe('ReportsFiltersInboxes.vue', () => {
|
||||
let store;
|
||||
let inboxesModule;
|
||||
|
||||
beforeEach(() => {
|
||||
inboxesModule = {
|
||||
namespaced: true,
|
||||
getters: {
|
||||
getInboxes: () => () => [
|
||||
{ id: 1, name: 'Inbox 1' },
|
||||
{ id: 2, name: 'Inbox 2' },
|
||||
],
|
||||
},
|
||||
actions: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
store = new Vuex.Store({
|
||||
modules: {
|
||||
inboxes: inboxesModule,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches "inboxes/get" action when component is mounted', () => {
|
||||
shallowMount(ReportsFiltersInboxes, {
|
||||
store,
|
||||
localVue,
|
||||
...mountParams,
|
||||
});
|
||||
expect(inboxesModule.actions.get).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('emits "inbox-filter-selection" event when handleInput is called', () => {
|
||||
const wrapper = shallowMount(ReportsFiltersInboxes, {
|
||||
store,
|
||||
localVue,
|
||||
...mountParams,
|
||||
});
|
||||
|
||||
const selectedInbox = { id: 1, name: 'Inbox 1' };
|
||||
wrapper.setData({ selectedOption: selectedInbox });
|
||||
|
||||
wrapper.vm.handleInput();
|
||||
|
||||
expect(wrapper.emitted('inbox-filter-selection')).toBeTruthy();
|
||||
expect(wrapper.emitted('inbox-filter-selection')[0]).toEqual([
|
||||
selectedInbox,
|
||||
]);
|
||||
});
|
||||
});
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import ReportsFiltersLabels from '../../Filters/Labels';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
const mountParams = {
|
||||
mocks: {
|
||||
$t: msg => msg,
|
||||
},
|
||||
stubs: ['multiselect'],
|
||||
};
|
||||
|
||||
describe('ReportsFiltersLabels.vue', () => {
|
||||
let store;
|
||||
let labelsModule;
|
||||
|
||||
beforeEach(() => {
|
||||
labelsModule = {
|
||||
namespaced: true,
|
||||
getters: {
|
||||
getLabels: () => () => [
|
||||
{ id: 1, title: 'Label 1', color: 'red' },
|
||||
{ id: 2, title: 'Label 2', color: 'blue' },
|
||||
],
|
||||
},
|
||||
actions: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
store = new Vuex.Store({
|
||||
modules: {
|
||||
labels: labelsModule,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches "labels/get" action when component is mounted', () => {
|
||||
shallowMount(ReportsFiltersLabels, {
|
||||
store,
|
||||
localVue,
|
||||
...mountParams,
|
||||
});
|
||||
expect(labelsModule.actions.get).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('emits "labels-filter-selection" event when handleInput is called', () => {
|
||||
const wrapper = shallowMount(ReportsFiltersLabels, {
|
||||
store,
|
||||
localVue,
|
||||
...mountParams,
|
||||
});
|
||||
|
||||
const selectedLabel = { id: 1, title: 'Label 1', color: 'red' };
|
||||
wrapper.setData({ selectedOption: selectedLabel });
|
||||
|
||||
wrapper.vm.handleInput();
|
||||
|
||||
expect(wrapper.emitted('labels-filter-selection')).toBeTruthy();
|
||||
expect(wrapper.emitted('labels-filter-selection')[0]).toEqual([
|
||||
selectedLabel,
|
||||
]);
|
||||
});
|
||||
});
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import ReportFiltersRatings from '../../Filters/Ratings';
|
||||
import { CSAT_RATINGS } from 'shared/constants/messages';
|
||||
|
||||
const mountParams = {
|
||||
mocks: {
|
||||
$t: msg => msg,
|
||||
},
|
||||
stubs: ['multiselect'],
|
||||
};
|
||||
|
||||
const localVue = createLocalVue();
|
||||
|
||||
describe('ReportFiltersRatings.vue', () => {
|
||||
it('emits "rating-filter-selection" event when handleInput is called', () => {
|
||||
const wrapper = shallowMount(ReportFiltersRatings, {
|
||||
localVue,
|
||||
...mountParams,
|
||||
});
|
||||
|
||||
const selectedRating = { value: 1, label: 'Rating 1' };
|
||||
wrapper.setData({ selectedOption: selectedRating });
|
||||
|
||||
wrapper.vm.handleInput(selectedRating);
|
||||
|
||||
expect(wrapper.emitted('rating-filter-selection')).toBeTruthy();
|
||||
expect(wrapper.emitted('rating-filter-selection')[0]).toEqual([
|
||||
selectedRating,
|
||||
]);
|
||||
});
|
||||
|
||||
it('initializes options correctly', () => {
|
||||
const wrapper = shallowMount(ReportFiltersRatings, {
|
||||
localVue,
|
||||
...mountParams,
|
||||
});
|
||||
|
||||
const expectedOptions = CSAT_RATINGS.map(option => ({
|
||||
...option,
|
||||
label: option.translationKey,
|
||||
}));
|
||||
|
||||
expect(wrapper.vm.options).toEqual(expectedOptions);
|
||||
});
|
||||
});
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import ReportsFiltersTeams from '../../Filters/Teams.vue';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
const mountParams = {
|
||||
mocks: {
|
||||
$t: msg => msg,
|
||||
},
|
||||
stubs: ['multiselect'],
|
||||
};
|
||||
|
||||
describe('ReportsFiltersTeams.vue', () => {
|
||||
let store;
|
||||
let teamsModule;
|
||||
|
||||
beforeEach(() => {
|
||||
teamsModule = {
|
||||
namespaced: true,
|
||||
getters: {
|
||||
getTeams: () => () => [
|
||||
{ id: 1, name: 'Team 1' },
|
||||
{ id: 2, name: 'Team 2' },
|
||||
],
|
||||
},
|
||||
actions: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
store = new Vuex.Store({
|
||||
modules: {
|
||||
teams: teamsModule,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches "teams/get" action when component is mounted', () => {
|
||||
shallowMount(ReportsFiltersTeams, {
|
||||
store,
|
||||
localVue,
|
||||
...mountParams,
|
||||
});
|
||||
expect(teamsModule.actions.get).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('emits "team-filter-selection" event when handleInput is called', () => {
|
||||
const wrapper = shallowMount(ReportsFiltersTeams, {
|
||||
store,
|
||||
localVue,
|
||||
...mountParams,
|
||||
});
|
||||
wrapper.setData({ selectedOption: { id: 1, name: 'Team 1' } });
|
||||
wrapper.vm.handleInput();
|
||||
expect(wrapper.emitted('team-filter-selection')).toBeTruthy();
|
||||
expect(wrapper.emitted('team-filter-selection')[0]).toEqual([
|
||||
{ id: 1, name: 'Team 1' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,85 @@ export const GROUP_BY_FILTER = {
|
||||
4: { id: 4, period: 'year' },
|
||||
};
|
||||
|
||||
export const GROUP_BY_OPTIONS = {
|
||||
DAY: {
|
||||
id: 'DAY',
|
||||
period: 'day',
|
||||
translationKey: 'REPORT.GROUPING_OPTIONS.DAY',
|
||||
},
|
||||
WEEK: {
|
||||
id: 'WEEK',
|
||||
period: 'week',
|
||||
translationKey: 'REPORT.GROUPING_OPTIONS.WEEK',
|
||||
},
|
||||
MONTH: {
|
||||
id: 'MONTH',
|
||||
period: 'month',
|
||||
translationKey: 'REPORT.GROUPING_OPTIONS.MONTH',
|
||||
},
|
||||
YEAR: {
|
||||
id: 'YEAR',
|
||||
period: 'year',
|
||||
translationKey: 'REPORT.GROUPING_OPTIONS.YEAR',
|
||||
},
|
||||
};
|
||||
|
||||
export const DATE_RANGE_OPTIONS = {
|
||||
LAST_7_DAYS: {
|
||||
id: 'LAST_7_DAYS',
|
||||
translationKey: 'REPORT.DATE_RANGE_OPTIONS.LAST_7_DAYS',
|
||||
offset: 6,
|
||||
groupByOptions: [GROUP_BY_OPTIONS.DAY],
|
||||
},
|
||||
LAST_30_DAYS: {
|
||||
id: 'LAST_30_DAYS',
|
||||
translationKey: 'REPORT.DATE_RANGE_OPTIONS.LAST_30_DAYS',
|
||||
offset: 29,
|
||||
groupByOptions: [GROUP_BY_OPTIONS.DAY, GROUP_BY_OPTIONS.WEEK],
|
||||
},
|
||||
LAST_3_MONTHS: {
|
||||
id: 'LAST_3_MONTHS',
|
||||
translationKey: 'REPORT.DATE_RANGE_OPTIONS.LAST_3_MONTHS',
|
||||
offset: 89,
|
||||
groupByOptions: [
|
||||
GROUP_BY_OPTIONS.DAY,
|
||||
GROUP_BY_OPTIONS.WEEK,
|
||||
GROUP_BY_OPTIONS.MONTH,
|
||||
],
|
||||
},
|
||||
LAST_6_MONTHS: {
|
||||
id: 'LAST_6_MONTHS',
|
||||
translationKey: 'REPORT.DATE_RANGE_OPTIONS.LAST_6_MONTHS',
|
||||
offset: 179,
|
||||
groupByOptions: [
|
||||
GROUP_BY_OPTIONS.DAY,
|
||||
GROUP_BY_OPTIONS.WEEK,
|
||||
GROUP_BY_OPTIONS.MONTH,
|
||||
],
|
||||
},
|
||||
LAST_YEAR: {
|
||||
id: 'LAST_YEAR',
|
||||
translationKey: 'REPORT.DATE_RANGE_OPTIONS.LAST_YEAR',
|
||||
offset: 364,
|
||||
groupByOptions: [
|
||||
GROUP_BY_OPTIONS.DAY,
|
||||
GROUP_BY_OPTIONS.WEEK,
|
||||
GROUP_BY_OPTIONS.MONTH,
|
||||
],
|
||||
},
|
||||
CUSTOM_DATE_RANGE: {
|
||||
id: 'CUSTOM_DATE_RANGE',
|
||||
translationKey: 'REPORT.DATE_RANGE_OPTIONS.CUSTOM_DATE_RANGE',
|
||||
offset: null,
|
||||
groupByOptions: [
|
||||
GROUP_BY_OPTIONS.DAY,
|
||||
GROUP_BY_OPTIONS.WEEK,
|
||||
GROUP_BY_OPTIONS.MONTH,
|
||||
GROUP_BY_OPTIONS.YEAR,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const CHART_FONT_FAMILY =
|
||||
'-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
|
||||
|
||||
|
||||
@@ -85,13 +85,10 @@ export const getters = {
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
get: async function getResponses(
|
||||
{ commit },
|
||||
{ page = 1, from, to, user_ids } = {}
|
||||
) {
|
||||
get: async function getResponses({ commit }, params) {
|
||||
commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetching: true });
|
||||
try {
|
||||
const response = await CSATReports.get({ page, from, to, user_ids });
|
||||
const response = await CSATReports.get(params);
|
||||
commit(types.SET_CSAT_RESPONSE, response.data);
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
@@ -99,10 +96,10 @@ export const actions = {
|
||||
commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetching: false });
|
||||
}
|
||||
},
|
||||
getMetrics: async function getMetrics({ commit }, { from, to, user_ids }) {
|
||||
getMetrics: async function getMetrics({ commit }, params) {
|
||||
commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetchingMetrics: true });
|
||||
try {
|
||||
const response = await CSATReports.getMetrics({ from, to, user_ids });
|
||||
const response = await CSATReports.getMetrics(params);
|
||||
commit(types.SET_CSAT_RESPONSE_METRICS, response.data);
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
|
||||
@@ -59,24 +59,28 @@ export const ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP =
|
||||
export const CSAT_RATINGS = [
|
||||
{
|
||||
key: 'disappointed',
|
||||
translationKey: 'CSAT.RATINGS.POOR',
|
||||
emoji: '😞',
|
||||
value: 1,
|
||||
color: '#FDAD2A',
|
||||
},
|
||||
{
|
||||
key: 'expressionless',
|
||||
translationKey: 'CSAT.RATINGS.FAIR',
|
||||
emoji: '😑',
|
||||
value: 2,
|
||||
color: '#FFC532',
|
||||
},
|
||||
{
|
||||
key: 'neutral',
|
||||
translationKey: 'CSAT.RATINGS.AVERAGE',
|
||||
emoji: '😐',
|
||||
value: 3,
|
||||
color: '#FCEC56',
|
||||
},
|
||||
{
|
||||
key: 'grinning',
|
||||
translationKey: 'CSAT.RATINGS.GOOD',
|
||||
emoji: '😀',
|
||||
value: 4,
|
||||
color: '#6FD86F',
|
||||
@@ -84,6 +88,7 @@ export const CSAT_RATINGS = [
|
||||
{
|
||||
key: 'smiling',
|
||||
emoji: '😍',
|
||||
translationKey: 'CSAT.RATINGS.EXCELLENT',
|
||||
value: 5,
|
||||
color: '#44CE4B',
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ import fromUnixTime from 'date-fns/fromUnixTime';
|
||||
import format from 'date-fns/format';
|
||||
import isToday from 'date-fns/isToday';
|
||||
import isYesterday from 'date-fns/isYesterday';
|
||||
import { endOfDay, getUnixTime, startOfDay } from 'date-fns';
|
||||
|
||||
export const formatUnixDate = (date, dateFormat = 'MMM dd, yyyy') => {
|
||||
const unixDate = fromUnixTime(date);
|
||||
@@ -31,8 +32,15 @@ export const isTimeAfter = (h1, m1, h2, m2) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
/** Get start of day as a UNIX timestamp */
|
||||
export const getUnixStartOfDay = date => getUnixTime(startOfDay(date));
|
||||
|
||||
/** Get end of day as a UNIX timestamp */
|
||||
export const getUnixEndOfDay = date => getUnixTime(endOfDay(date));
|
||||
|
||||
export const generateRelativeTime = (value, unit, languageCode) => {
|
||||
const rtf = new Intl.RelativeTimeFormat(languageCode, {
|
||||
const code = languageCode?.replace(/_/g, '-'); // Hacky fix we need to handle it from source
|
||||
const rtf = new Intl.RelativeTimeFormat(code, {
|
||||
numeric: 'auto',
|
||||
});
|
||||
return rtf.format(value, unit);
|
||||
|
||||
@@ -64,15 +64,59 @@ describe('#isTimeAfter', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#generateRelativeTime', () => {
|
||||
it('should return correct relative time', () => {
|
||||
expect(generateRelativeTime(-1, 'day', 'en')).toEqual('yesterday');
|
||||
expect(generateRelativeTime(1, 'day', 'en')).toEqual('tomorrow');
|
||||
expect(generateRelativeTime(1, 'hour', 'en')).toEqual('in 1 hour');
|
||||
expect(generateRelativeTime(-1, 'hour', 'en')).toEqual('1 hour ago');
|
||||
expect(generateRelativeTime(1, 'minute', 'en')).toEqual('in 1 minute');
|
||||
expect(generateRelativeTime(-1, 'minute', 'en')).toEqual('1 minute ago');
|
||||
expect(generateRelativeTime(1, 'second', 'en')).toEqual('in 1 second');
|
||||
expect(generateRelativeTime(-1, 'second', 'en')).toEqual('1 second ago');
|
||||
describe('generateRelativeTime', () => {
|
||||
it('should return a string with the relative time', () => {
|
||||
const value = 1;
|
||||
const unit = 'second';
|
||||
const languageCode = 'en-US';
|
||||
const expectedResult = 'in 1 second';
|
||||
|
||||
const actualResult = generateRelativeTime(value, unit, languageCode);
|
||||
|
||||
expect(actualResult).toBe(expectedResult);
|
||||
});
|
||||
|
||||
it('should return a string with the relative time in a different language', () => {
|
||||
const value = 10;
|
||||
const unit = 'minute';
|
||||
const languageCode = 'de-DE';
|
||||
const expectedResult = 'in 10 Minuten';
|
||||
|
||||
const actualResult = generateRelativeTime(value, unit, languageCode);
|
||||
|
||||
expect(actualResult).toBe(expectedResult);
|
||||
});
|
||||
|
||||
it('should return a string with the relative time for a different unit', () => {
|
||||
const value = 1;
|
||||
const unit = 'hour';
|
||||
const languageCode = 'en-US';
|
||||
const expectedResult = 'in 1 hour';
|
||||
|
||||
const actualResult = generateRelativeTime(value, unit, languageCode);
|
||||
|
||||
expect(actualResult).toBe(expectedResult);
|
||||
});
|
||||
|
||||
it('should throw an error if the value is not a number', () => {
|
||||
const value = 1;
|
||||
const unit = 'day';
|
||||
const languageCode = 'en_US';
|
||||
const expectedResult = 'tomorrow';
|
||||
|
||||
const actualResult = generateRelativeTime(value, unit, languageCode);
|
||||
|
||||
expect(actualResult).toBe(expectedResult);
|
||||
});
|
||||
|
||||
it('should throw an error if the value is not a number', () => {
|
||||
const value = 1;
|
||||
const unit = 'day';
|
||||
const languageCode = 'en-US';
|
||||
const expectedResult = 'tomorrow';
|
||||
|
||||
const actualResult = generateRelativeTime(value, unit, languageCode);
|
||||
|
||||
expect(actualResult).toBe(expectedResult);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,6 +31,6 @@ class Webhooks::InstagramEventsJob < ApplicationJob
|
||||
end
|
||||
|
||||
def messages(entry)
|
||||
(entry[:messaging].presence || entry[:standby])
|
||||
(entry[:messaging].presence || entry[:standby] || [])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,7 +6,7 @@ module Featurable
|
||||
check_for_column: false
|
||||
}.freeze
|
||||
|
||||
FEATURE_LIST = YAML.safe_load(File.read(Rails.root.join('config/features.yml'))).freeze
|
||||
FEATURE_LIST = YAML.safe_load(Rails.root.join('config/features.yml').read).freeze
|
||||
|
||||
FEATURES = FEATURE_LIST.each_with_object({}) do |feature, result|
|
||||
result[result.keys.size + 1] = "feature_#{feature['name']}".to_sym
|
||||
@@ -46,7 +46,7 @@ module Featurable
|
||||
end
|
||||
|
||||
def all_features
|
||||
FEATURE_LIST.map { |f| f['name'] }.index_with do |feature_name|
|
||||
FEATURE_LIST.pluck('name').index_with do |feature_name|
|
||||
feature_enabled?(feature_name)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -233,8 +233,8 @@ class Conversation < ApplicationRecord
|
||||
|
||||
def allowed_keys?
|
||||
(
|
||||
(previous_changes.keys & %w[team_id assignee_id status snoozed_until custom_attributes label_list first_reply_created_at priority]).present? ||
|
||||
(previous_changes['additional_attributes'].present? && (previous_changes['additional_attributes'][1].keys & %w[conversation_language]).present?)
|
||||
previous_changes.keys.intersect?(%w[team_id assignee_id status snoozed_until custom_attributes label_list first_reply_created_at priority]) ||
|
||||
(previous_changes['additional_attributes'].present? && previous_changes['additional_attributes'][1].keys.intersect?(%w[conversation_language]))
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -35,4 +35,8 @@ class CsatSurveyResponse < ApplicationRecord
|
||||
|
||||
scope :filter_by_created_at, ->(range) { where(created_at: range) if range.present? }
|
||||
scope :filter_by_assigned_agent_id, ->(user_ids) { where(assigned_agent_id: user_ids) if user_ids.present? }
|
||||
scope :filter_by_inbox_id, ->(inbox_id) { joins(:conversation).where(conversations: { inbox_id: inbox_id }) if inbox_id.present? }
|
||||
scope :filter_by_team_id, ->(team_id) { joins(:conversation).where(conversations: { team_id: team_id }) if team_id.present? }
|
||||
# filter by rating value
|
||||
scope :filter_by_rating, ->(rating) { where(rating: rating) if rating.present? }
|
||||
end
|
||||
|
||||
@@ -19,7 +19,7 @@ class InstallationConfig < ApplicationRecord
|
||||
# https://discuss.rubyonrails.org/t/cve-2022-32224-possible-rce-escalation-bug-with-serialized-columns-in-active-record/81017
|
||||
# FIX ME : fixes breakage of installation config. we need to migrate.
|
||||
# Fix configuration in application.rb
|
||||
serialize :serialized_value, HashWithIndifferentAccess
|
||||
serialize :serialized_value, ActiveSupport::HashWithIndifferentAccess
|
||||
|
||||
before_validation :set_lock
|
||||
validates :name, presence: true
|
||||
|
||||
@@ -8,11 +8,12 @@ json.position article.position
|
||||
json.account_id article.account_id
|
||||
json.updated_at article.updated_at.to_i
|
||||
json.meta article.meta
|
||||
|
||||
json.category do
|
||||
json.id article.category_id
|
||||
json.name article.category.name
|
||||
json.slug article.category.slug
|
||||
json.locale article.category.locale
|
||||
json.name article.category&.name
|
||||
json.slug article.category&.slug
|
||||
json.locale article.category&.locale
|
||||
end
|
||||
|
||||
json.views article.views
|
||||
|
||||
@@ -4,7 +4,7 @@ json.contact_last_seen_at resource.contact_last_seen_at.to_i
|
||||
json.status resource.status
|
||||
json.agent_last_seen_at resource.agent_last_seen_at.to_i
|
||||
json.messages do
|
||||
json.array! resource.messages do |message|
|
||||
json.array! resource.messages.chat do |message|
|
||||
json.partial! 'public/api/v1/models/message', formats: [:json], resource: message
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,5 +11,5 @@ ActionCable::SubscriptionAdapter::Redis.redis_connector = lambda do |config|
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75173
|
||||
# https://github.com/rails/rails/blob/4a23cb3415eac03d76623112576559a722d1f23d/actioncable/lib/action_cable/subscription_adapter/base.rb#L30
|
||||
config[:id] = nil if ENV['REDIS_DISABLE_CLIENT_COMMAND'].present?
|
||||
::Redis.new(config.except(:adapter, :channel_prefix))
|
||||
Redis.new(config.except(:adapter, :channel_prefix))
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class InstallAudited < ActiveRecord::Migration[6.1]
|
||||
# rubocop:disable RMetrics/MethodLength
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
def self.up
|
||||
create_table :audits, :force => true do |t|
|
||||
t.bigint :auditable_id
|
||||
@@ -19,7 +19,7 @@ class InstallAudited < ActiveRecord::Migration[6.1]
|
||||
t.string :request_uuid
|
||||
t.datetime :created_at
|
||||
end
|
||||
# rubocop:enable RMetrics/MethodLength
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
|
||||
add_index :audits, [:auditable_type, :auditable_id, :version], :name => 'auditable_index'
|
||||
add_index :audits, [:associated_type, :associated_id], :name => 'associated_index'
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@ ConfigLoader.new.process
|
||||
## Seeds productions
|
||||
if Rails.env.production?
|
||||
# Setup Onboarding flow
|
||||
::Redis::Alfred.set(::Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING, true)
|
||||
Redis::Alfred.set(Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING, true)
|
||||
end
|
||||
|
||||
## Seeds for Local Development
|
||||
@@ -46,7 +46,7 @@ unless Rails.env.production?
|
||||
inbox = Inbox.create!(channel: web_widget, account: account, name: 'Acme Support')
|
||||
InboxMember.create!(user: user, inbox: inbox)
|
||||
|
||||
contact_inbox = ::ContactInboxWithContactBuilder.new(
|
||||
contact_inbox = ContactInboxWithContactBuilder.new(
|
||||
source_id: user.id,
|
||||
inbox: inbox,
|
||||
hmac_verified: true,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# Description: Install and manage a Chatwoot installation.
|
||||
# OS: Ubuntu 20.04 LTS
|
||||
# Script Version: 2.2.0
|
||||
# Script Version: 2.3.0
|
||||
# Run this script as root
|
||||
|
||||
set -eu -o errexit -o pipefail -o noclobber -o nounset
|
||||
@@ -19,7 +19,7 @@ fi
|
||||
# option --output/-o requires 1 argument
|
||||
LONGOPTS=console,debug,help,install,Install:,logs:,restart,ssl,upgrade,webserver,version
|
||||
OPTIONS=cdhiI:l:rsuwv
|
||||
CWCTL_VERSION="2.2.0"
|
||||
CWCTL_VERSION="2.3.0"
|
||||
pg_pass=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 15 ; echo '')
|
||||
|
||||
# if user does not specify an option
|
||||
@@ -175,6 +175,8 @@ function install_dependencies() {
|
||||
curl -sL https://deb.nodesource.com/setup_16.x | bash -
|
||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
|
||||
curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list
|
||||
apt update
|
||||
|
||||
apt install -y \
|
||||
@@ -183,7 +185,8 @@ function install_dependencies() {
|
||||
libssl-dev libyaml-dev libreadline-dev gnupg2 \
|
||||
postgresql-client redis-tools \
|
||||
nodejs yarn patch ruby-dev zlib1g-dev liblzma-dev \
|
||||
libgmp-dev libncurses5-dev libffi-dev libgdbm6 libgdbm-dev sudo
|
||||
libgmp-dev libncurses5-dev libffi-dev libgdbm6 libgdbm-dev sudo \
|
||||
libvips
|
||||
}
|
||||
|
||||
##############################################################################
|
||||
@@ -708,6 +711,26 @@ function upgrade_prereq() {
|
||||
EOF
|
||||
}
|
||||
|
||||
##############################################################################
|
||||
# Update redis to v7+ for Rails 7 support(-u/--upgrade)
|
||||
# and install libvips for image processing support in Rails 7
|
||||
# Globals:
|
||||
# None
|
||||
# Arguments:
|
||||
# None
|
||||
# Outputs:
|
||||
# None
|
||||
##############################################################################
|
||||
function upgrade_redis() {
|
||||
echo "Upgrading Redis to v7+ for Rails 7 support(Chatwoot v2.17+)"
|
||||
curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list
|
||||
apt update -y
|
||||
apt upgrade redis-server -y
|
||||
apt install libvips -y
|
||||
}
|
||||
|
||||
|
||||
##############################################################################
|
||||
# Upgrade an existing installation to latest stable version(-u/--upgrade)
|
||||
# Globals:
|
||||
@@ -722,6 +745,7 @@ function upgrade() {
|
||||
echo "Upgrading Chatwoot to v$CW_VERSION"
|
||||
sleep 3
|
||||
upgrade_prereq
|
||||
upgrade_redis
|
||||
sudo -i -u chatwoot << "EOF"
|
||||
|
||||
# Navigate to the Chatwoot directory
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CustomExceptions::Base < ::StandardError
|
||||
class CustomExceptions::Base < StandardError
|
||||
def to_hash
|
||||
{
|
||||
message: message
|
||||
|
||||
@@ -28,7 +28,8 @@ class Integrations::Openai::ProcessorService
|
||||
model: GPT_MODEL,
|
||||
messages: [
|
||||
{ role: 'system',
|
||||
content: "You are a helpful support agent. Please rephrase the following response to a more #{event['data']['tone']} tone." },
|
||||
content: "You are a helpful support agent. Please rephrase the following response to a more #{event['data']['tone']} tone. " \
|
||||
"Reply in the user's language." },
|
||||
{ role: 'user', content: event['data']['content'] }
|
||||
]
|
||||
}.to_json
|
||||
@@ -96,7 +97,7 @@ class Integrations::Openai::ProcessorService
|
||||
messages: [
|
||||
{ role: 'system',
|
||||
content: 'Please summarize the key points from the following conversation between support agents and ' \
|
||||
'customer as bullet points for the next support agent looking into the conversation' },
|
||||
'customer as bullet points for the next support agent looking into the conversation. Reply in the user\'s language.' },
|
||||
{ role: 'user', content: conversation_messages }
|
||||
]
|
||||
}.to_json
|
||||
@@ -106,7 +107,8 @@ class Integrations::Openai::ProcessorService
|
||||
{
|
||||
model: GPT_MODEL,
|
||||
messages: [
|
||||
{ role: 'system', content: 'Please suggest a reply to the following conversation between support agents and customer' }
|
||||
{ role: 'system',
|
||||
content: 'Please suggest a reply to the following conversation between support agents and customer. Reply in the user\'s language.' }
|
||||
].concat(conversation_messages(in_array_format: true))
|
||||
}.to_json
|
||||
end
|
||||
|
||||
@@ -12,7 +12,7 @@ class Seeders::AccountSeeder
|
||||
def initialize(account:)
|
||||
raise 'Account Seeding is not allowed.' unless ENV.fetch('ENABLE_ACCOUNT_SEEDING', !Rails.env.production?)
|
||||
|
||||
@account_data = HashWithIndifferentAccess.new(YAML.safe_load(File.read(Rails.root.join('lib/seeders/seed_data.yml'))))
|
||||
@account_data = ActiveSupport::HashWithIndifferentAccess.new(YAML.safe_load(Rails.root.join('lib/seeders/seed_data.yml').read))
|
||||
@account = account
|
||||
end
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ::ContactIdentifyAction do
|
||||
describe ContactIdentifyAction do
|
||||
subject(:contact_identify) { described_class.new(contact: contact, params: params).perform }
|
||||
|
||||
let!(:account) { create(:account) }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ::ContactMergeAction do
|
||||
describe ContactMergeAction do
|
||||
subject(:contact_merge) { described_class.new(account: account, base_contact: base_contact, mergee_contact: mergee_contact).perform }
|
||||
|
||||
let!(:account) { create(:account) }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ::Campaigns::CampaignConversationBuilder do
|
||||
describe Campaigns::CampaignConversationBuilder do
|
||||
let(:account) { create(:account) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:contact) { create(:contact, account: account, identifier: '123') }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ::ContactInboxBuilder do
|
||||
describe ContactInboxBuilder do
|
||||
let(:account) { create(:account) }
|
||||
let(:contact) { create(:contact, email: 'xyc@example.com', phone_number: '+23423424123', account: account) }
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ::ContactInboxWithContactBuilder do
|
||||
describe ContactInboxWithContactBuilder do
|
||||
let(:account) { create(:account) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:contact) { create(:contact, email: 'xyc@example.com', phone_number: '+23423424123', account: account, identifier: '123') }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ::ConversationBuilder do
|
||||
describe ConversationBuilder do
|
||||
let(:account) { create(:account) }
|
||||
let!(:sms_channel) { create(:channel_sms, account: account) }
|
||||
let!(:sms_inbox) { create(:inbox, channel: sms_channel, account: account) }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ::CsatSurveys::ResponseBuilder do
|
||||
describe CsatSurveys::ResponseBuilder do
|
||||
let(:message) do
|
||||
create(
|
||||
:message, content_type: :input_csat,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ::Messages::Facebook::MessageBuilder do
|
||||
describe Messages::Facebook::MessageBuilder do
|
||||
subject(:message_builder) { described_class.new(incoming_fb_text_message, facebook_channel.inbox).perform }
|
||||
|
||||
before do
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ::Messages::Instagram::MessageBuilder do
|
||||
describe Messages::Instagram::MessageBuilder do
|
||||
subject(:instagram_message_builder) { described_class }
|
||||
|
||||
before do
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ::Messages::MessageBuilder do
|
||||
describe Messages::MessageBuilder do
|
||||
subject(:message_builder) { described_class.new(user, conversation, params).perform }
|
||||
|
||||
let(:account) { create(:account) }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ::NotificationBuilder do
|
||||
describe NotificationBuilder do
|
||||
include ActiveJob::TestHelper
|
||||
|
||||
describe '#perform' do
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ::V2::ReportBuilder do
|
||||
describe V2::ReportBuilder do
|
||||
include ActiveJob::TestHelper
|
||||
let_it_be(:account) { create(:account) }
|
||||
let_it_be(:label_1) { create(:label, title: 'Label_1', account: account) }
|
||||
@@ -60,7 +60,7 @@ describe ::V2::ReportBuilder do
|
||||
until: Time.zone.today.end_of_day.to_time.to_i.to_s
|
||||
}
|
||||
|
||||
builder = V2::ReportBuilder.new(account, params)
|
||||
builder = described_class.new(account, params)
|
||||
metrics = builder.timeseries
|
||||
|
||||
expect(metrics[Time.zone.today]).to be 10
|
||||
@@ -75,7 +75,7 @@ describe ::V2::ReportBuilder do
|
||||
until: Time.zone.today.end_of_day.to_time.to_i.to_s
|
||||
}
|
||||
|
||||
builder = V2::ReportBuilder.new(account, params)
|
||||
builder = described_class.new(account, params)
|
||||
metrics = builder.timeseries
|
||||
|
||||
expect(metrics[Time.zone.today]).to be 20
|
||||
@@ -90,7 +90,7 @@ describe ::V2::ReportBuilder do
|
||||
until: Time.zone.today.end_of_day.to_time.to_i.to_s
|
||||
}
|
||||
|
||||
builder = V2::ReportBuilder.new(account, params)
|
||||
builder = described_class.new(account, params)
|
||||
metrics = builder.timeseries
|
||||
|
||||
expect(metrics[Time.zone.today]).to be 50
|
||||
@@ -107,7 +107,7 @@ describe ::V2::ReportBuilder do
|
||||
|
||||
conversations = account.conversations.where('created_at < ?', 1.day.ago)
|
||||
conversations.each(&:resolved!)
|
||||
builder = V2::ReportBuilder.new(account, params)
|
||||
builder = described_class.new(account, params)
|
||||
metrics = builder.timeseries
|
||||
|
||||
expect(metrics[Time.zone.today]).to be 0
|
||||
@@ -122,7 +122,7 @@ describe ::V2::ReportBuilder do
|
||||
until: Time.zone.today.end_of_day.to_time.to_i.to_s
|
||||
}
|
||||
|
||||
builder = V2::ReportBuilder.new(account, params)
|
||||
builder = described_class.new(account, params)
|
||||
metrics = builder.timeseries
|
||||
|
||||
expect(metrics[Time.zone.today].to_f).to be 0.48e4
|
||||
@@ -135,7 +135,7 @@ describe ::V2::ReportBuilder do
|
||||
until: Time.zone.today.end_of_day.to_time.to_i.to_s
|
||||
}
|
||||
|
||||
builder = V2::ReportBuilder.new(account, params)
|
||||
builder = described_class.new(account, params)
|
||||
metrics = builder.summary
|
||||
|
||||
expect(metrics[:conversations_count]).to be 15
|
||||
@@ -153,7 +153,7 @@ describe ::V2::ReportBuilder do
|
||||
group_by: 'test'.to_s
|
||||
}
|
||||
|
||||
builder = V2::ReportBuilder.new(account, params)
|
||||
builder = described_class.new(account, params)
|
||||
expect { builder.summary }.to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
@@ -168,7 +168,7 @@ describe ::V2::ReportBuilder do
|
||||
until: Time.zone.today.end_of_day.to_time.to_i.to_s
|
||||
}
|
||||
|
||||
builder = V2::ReportBuilder.new(account, params)
|
||||
builder = described_class.new(account, params)
|
||||
metrics = builder.timeseries
|
||||
|
||||
expect(metrics[Time.zone.today - 2.days]).to be 5
|
||||
@@ -183,7 +183,7 @@ describe ::V2::ReportBuilder do
|
||||
until: (Time.zone.today + 1.day).to_time.to_i.to_s
|
||||
}
|
||||
|
||||
builder = V2::ReportBuilder.new(account, params)
|
||||
builder = described_class.new(account, params)
|
||||
metrics = builder.timeseries
|
||||
|
||||
expect(metrics[Time.zone.today]).to be 20
|
||||
@@ -199,7 +199,7 @@ describe ::V2::ReportBuilder do
|
||||
until: (Time.zone.today + 1.day).to_time.to_i.to_s
|
||||
}
|
||||
|
||||
builder = V2::ReportBuilder.new(account, params)
|
||||
builder = described_class.new(account, params)
|
||||
metrics = builder.timeseries
|
||||
|
||||
expect(metrics[Time.zone.today]).to be 50
|
||||
@@ -217,7 +217,7 @@ describe ::V2::ReportBuilder do
|
||||
|
||||
conversations = account.conversations.where('created_at < ?', 1.day.ago)
|
||||
conversations.each(&:resolved!)
|
||||
builder = V2::ReportBuilder.new(account, params)
|
||||
builder = described_class.new(account, params)
|
||||
metrics = builder.timeseries
|
||||
|
||||
expect(metrics[Time.zone.today - 2.days]).to be 5
|
||||
@@ -234,7 +234,7 @@ describe ::V2::ReportBuilder do
|
||||
until: Time.zone.today.end_of_day.to_time.to_i.to_s
|
||||
}
|
||||
|
||||
builder = V2::ReportBuilder.new(account, params)
|
||||
builder = described_class.new(account, params)
|
||||
metrics = builder.timeseries
|
||||
expect(metrics[Time.zone.today].to_f).to be 0.15e1
|
||||
end
|
||||
@@ -247,7 +247,7 @@ describe ::V2::ReportBuilder do
|
||||
until: Time.zone.today.end_of_day.to_time.to_i.to_s
|
||||
}
|
||||
|
||||
builder = V2::ReportBuilder.new(account, params)
|
||||
builder = described_class.new(account, params)
|
||||
metrics = builder.summary
|
||||
|
||||
expect(metrics[:conversations_count]).to be 5
|
||||
@@ -266,7 +266,7 @@ describe ::V2::ReportBuilder do
|
||||
group_by: 'week'.to_s
|
||||
}
|
||||
|
||||
builder = V2::ReportBuilder.new(account, params)
|
||||
builder = described_class.new(account, params)
|
||||
metrics = builder.summary
|
||||
|
||||
expect(metrics[:conversations_count]).to be 5
|
||||
@@ -285,7 +285,7 @@ describe ::V2::ReportBuilder do
|
||||
group_by: 'test'.to_s
|
||||
}
|
||||
|
||||
builder = V2::ReportBuilder.new(account, params)
|
||||
builder = described_class.new(account, params)
|
||||
expect { builder.summary }.to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe RoomChannel, type: :channel do
|
||||
RSpec.describe RoomChannel do
|
||||
let!(:contact_inbox) { create(:contact_inbox) }
|
||||
|
||||
before do
|
||||
|
||||
@@ -19,6 +19,6 @@ RSpec.context 'with valid schedule.yml' do
|
||||
end
|
||||
end
|
||||
# ensure that no duplicates exist
|
||||
assert_equal schedule_keys.uniq.count, schedule_keys.count
|
||||
expect(schedule_keys.count).to eq(schedule_keys.uniq.count)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -22,7 +22,7 @@ RSpec.describe 'API Base', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['id']).to eq(user.id)
|
||||
expect(json_response['email']).to eq(user.email)
|
||||
end
|
||||
|
||||
@@ -30,7 +30,7 @@ RSpec.describe 'Contact Merge Action API', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['id']).to eq(base_contact.id)
|
||||
expected_params = { account: account, base_contact: base_contact, mergee_contact: mergee_contact }
|
||||
expect(ContactMergeAction).to have_received(:new).with(expected_params)
|
||||
|
||||
@@ -25,7 +25,7 @@ RSpec.describe 'Agents API', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(JSON.parse(response.body).size).to eq(account.users.count)
|
||||
expect(response.parsed_body.size).to eq(account.users.count)
|
||||
end
|
||||
|
||||
it 'returns custom fields on agents if present' do
|
||||
@@ -36,7 +36,7 @@ RSpec.describe 'Agents API', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
data = JSON.parse(response.body)
|
||||
data = response.parsed_body
|
||||
expect(data.first['custom_attributes']['test']).to eq('test')
|
||||
end
|
||||
end
|
||||
@@ -131,7 +131,7 @@ RSpec.describe 'Agents API', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_data = JSON.parse(response.body)
|
||||
response_data = response.parsed_body
|
||||
expect(response_data['role']).to eq('administrator')
|
||||
expect(response_data['availability_status']).to eq('busy')
|
||||
expect(response_data['auto_offline']).to be(false)
|
||||
|
||||
@@ -35,7 +35,30 @@ RSpec.describe 'Api::V1::Accounts::Articles', type: :request do
|
||||
params: article_params,
|
||||
headers: agent.create_new_auth_token
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['payload']['title']).to eql('MyTitle')
|
||||
expect(json_response['payload']['status']).to eql('draft')
|
||||
expect(json_response['payload']['position']).to be(3)
|
||||
end
|
||||
|
||||
it 'creates article even if category is not provided' do
|
||||
article_params = {
|
||||
article: {
|
||||
category_id: nil,
|
||||
description: 'test description',
|
||||
title: 'MyTitle',
|
||||
slug: 'my-title',
|
||||
content: 'This is my content.',
|
||||
status: :published,
|
||||
author_id: agent.id,
|
||||
position: 3
|
||||
}
|
||||
}
|
||||
post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles",
|
||||
params: article_params,
|
||||
headers: agent.create_new_auth_token
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['payload']['title']).to eql('MyTitle')
|
||||
expect(json_response['payload']['status']).to eql('draft')
|
||||
expect(json_response['payload']['position']).to be(3)
|
||||
@@ -63,7 +86,7 @@ RSpec.describe 'Api::V1::Accounts::Articles', type: :request do
|
||||
params: article_params,
|
||||
headers: agent.create_new_auth_token
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['payload']['title']).to eql('MyTitle')
|
||||
|
||||
category = Article.find(json_response['payload']['id'])
|
||||
@@ -89,7 +112,7 @@ RSpec.describe 'Api::V1::Accounts::Articles', type: :request do
|
||||
params: article_params,
|
||||
headers: agent.create_new_auth_token
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['payload']['title']).to eql('MyTitle')
|
||||
|
||||
category = Article.find(json_response['payload']['id'])
|
||||
@@ -123,7 +146,7 @@ RSpec.describe 'Api::V1::Accounts::Articles', type: :request do
|
||||
params: article_params,
|
||||
headers: agent.create_new_auth_token
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['payload']['title']).to eql(article_params[:article][:title])
|
||||
expect(json_response['payload']['status']).to eql(article_params[:article][:status])
|
||||
expect(json_response['payload']['position']).to eql(article_params[:article][:position])
|
||||
@@ -167,7 +190,7 @@ RSpec.describe 'Api::V1::Accounts::Articles', type: :request do
|
||||
headers: agent.create_new_auth_token,
|
||||
params: {}
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['payload'].count).to be 2
|
||||
end
|
||||
|
||||
@@ -179,7 +202,7 @@ RSpec.describe 'Api::V1::Accounts::Articles', type: :request do
|
||||
headers: agent.create_new_auth_token,
|
||||
params: { category_slug: category.slug }
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['payload'].count).to be 2
|
||||
end
|
||||
|
||||
@@ -196,7 +219,7 @@ RSpec.describe 'Api::V1::Accounts::Articles', type: :request do
|
||||
headers: agent.create_new_auth_token,
|
||||
params: { query: 'funny' }
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['payload'].count).to be 1
|
||||
expect(json_response['meta']['all_articles_count']).to be 2
|
||||
expect(json_response['meta']['articles_count']).to be 1
|
||||
@@ -212,7 +235,7 @@ RSpec.describe 'Api::V1::Accounts::Articles', type: :request do
|
||||
get "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles/#{article2.id}",
|
||||
headers: agent.create_new_auth_token
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
|
||||
expect(json_response['payload']['title']).to eq(article2.title)
|
||||
expect(json_response['payload']['id']).to eq(article2.id)
|
||||
@@ -228,11 +251,11 @@ RSpec.describe 'Api::V1::Accounts::Articles', type: :request do
|
||||
get "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles/#{root_article.id}",
|
||||
headers: agent.create_new_auth_token
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
|
||||
expect(json_response['payload']['associated_articles'].length).to eq(2)
|
||||
associated_articles_ids = json_response['payload']['associated_articles'].map { |article| article['id'] }
|
||||
expect(associated_articles_ids).to match_array([child_article_1.id, child_article_2.id])
|
||||
associated_articles_ids = json_response['payload']['associated_articles'].pluck('id')
|
||||
expect(associated_articles_ids).to contain_exactly(child_article_1.id, child_article_2.id)
|
||||
expect(json_response['payload']['id']).to eq(root_article.id)
|
||||
end
|
||||
end
|
||||
@@ -249,7 +272,7 @@ RSpec.describe 'Api::V1::Accounts::Articles', type: :request do
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
blob = JSON.parse(response.body)
|
||||
blob = response.parsed_body
|
||||
|
||||
expect(blob['file_url']).to be_present
|
||||
end
|
||||
|
||||
@@ -134,7 +134,7 @@ RSpec.describe 'Api::V1::Accounts::AutomationRulesController', type: :request do
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
blob = JSON.parse(response.body)
|
||||
blob = response.parsed_body
|
||||
|
||||
expect(blob['blob_key']).to be_present
|
||||
expect(blob['blob_id']).to be_present
|
||||
@@ -167,13 +167,13 @@ RSpec.describe 'Api::V1::Accounts::AutomationRulesController', type: :request do
|
||||
headers: administrator.create_new_auth_token,
|
||||
params: { attachment: file_1 }
|
||||
|
||||
blob_1 = JSON.parse(response.body)
|
||||
blob_1 = response.parsed_body
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/automation_rules/attach_file",
|
||||
headers: administrator.create_new_auth_token,
|
||||
params: { attachment: file_2 }
|
||||
|
||||
blob_2 = JSON.parse(response.body)
|
||||
blob_2 = response.parsed_body
|
||||
|
||||
params[:actions] = [
|
||||
{
|
||||
|
||||
@@ -222,7 +222,7 @@ RSpec.describe 'Campaigns API', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(::Campaign.exists?(campaign.display_id)).to be false
|
||||
expect(Campaign.exists?(campaign.display_id)).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,7 +25,7 @@ RSpec.describe 'Canned Responses API', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(JSON.parse(response.body)).to eq(account.canned_responses.as_json)
|
||||
expect(response.parsed_body).to eq(account.canned_responses.as_json)
|
||||
end
|
||||
|
||||
it 'returns all the canned responses the user searched for' do
|
||||
@@ -42,7 +42,7 @@ RSpec.describe 'Canned Responses API', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(JSON.parse(response.body)).to eq(
|
||||
expect(response.parsed_body).to eq(
|
||||
[cr3, cr2, cr1].as_json
|
||||
)
|
||||
end
|
||||
|
||||
@@ -62,7 +62,7 @@ RSpec.describe 'Api::V1::Accounts::Categories', type: :request do
|
||||
headers: agent.create_new_auth_token
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
|
||||
expect(json_response['payload']['related_categories'][0]['id']).to eql(related_category_1.id)
|
||||
expect(json_response['payload']['related_categories'][1]['id']).to eql(related_category_2.id)
|
||||
@@ -107,7 +107,7 @@ RSpec.describe 'Api::V1::Accounts::Categories', type: :request do
|
||||
params: category_params,
|
||||
headers: agent.create_new_auth_token
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['message']).to eql('Locale should be unique in the category and portal')
|
||||
end
|
||||
|
||||
@@ -125,7 +125,7 @@ RSpec.describe 'Api::V1::Accounts::Categories', type: :request do
|
||||
params: category_params,
|
||||
headers: agent.create_new_auth_token
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
|
||||
expect(json_response['message']).to eql("Slug can't be blank")
|
||||
end
|
||||
@@ -160,7 +160,7 @@ RSpec.describe 'Api::V1::Accounts::Categories', type: :request do
|
||||
params: category_params,
|
||||
headers: agent.create_new_auth_token
|
||||
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
|
||||
expect(json_response['payload']['name']).to eql(category_params[:category][:name])
|
||||
expect(json_response['payload']['related_categories'][0]['id']).to eql(related_category_1.id)
|
||||
@@ -185,7 +185,7 @@ RSpec.describe 'Api::V1::Accounts::Categories', type: :request do
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
|
||||
expect(json_response['payload']['name']).to eql(category.name)
|
||||
expect(json_response['payload']['related_categories'][0]['id']).to eql(related_category_1.id)
|
||||
@@ -257,7 +257,7 @@ RSpec.describe 'Api::V1::Accounts::Categories', type: :request do
|
||||
get "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories",
|
||||
headers: agent.create_new_auth_token
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['payload'].count).to be(category_count + 1)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,13 +4,13 @@ RSpec.describe '/api/v1/accounts/{account.id}/channels/twilio_channel', type: :r
|
||||
let(:account) { create(:account) }
|
||||
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:twilio_client) { instance_double(::Twilio::REST::Client) }
|
||||
let(:twilio_client) { instance_double(Twilio::REST::Client) }
|
||||
let(:message_double) { double }
|
||||
let(:twilio_webhook_setup_service) { instance_double(::Twilio::WebhookSetupService) }
|
||||
let(:twilio_webhook_setup_service) { instance_double(Twilio::WebhookSetupService) }
|
||||
|
||||
before do
|
||||
allow(::Twilio::REST::Client).to receive(:new).and_return(twilio_client)
|
||||
allow(::Twilio::WebhookSetupService).to receive(:new).and_return(twilio_webhook_setup_service)
|
||||
allow(Twilio::REST::Client).to receive(:new).and_return(twilio_client)
|
||||
allow(Twilio::WebhookSetupService).to receive(:new).and_return(twilio_webhook_setup_service)
|
||||
allow(twilio_webhook_setup_service).to receive(:perform)
|
||||
end
|
||||
|
||||
@@ -46,7 +46,7 @@ RSpec.describe '/api/v1/accounts/{account.id}/channels/twilio_channel', type: :r
|
||||
headers: admin.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
|
||||
expect(json_response['name']).to eq('SMS Channel')
|
||||
expect(json_response['messaging_service_sid']).to eq('MGec8130512b5dd462cfe03095ec1342ed')
|
||||
@@ -71,7 +71,7 @@ RSpec.describe '/api/v1/accounts/{account.id}/channels/twilio_channel', type: :r
|
||||
headers: admin.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
|
||||
expect(json_response['messaging_service_sid']).to eq('MGec8130512b5dd462cfe03095ec1111ed')
|
||||
end
|
||||
@@ -99,7 +99,7 @@ RSpec.describe '/api/v1/accounts/{account.id}/channels/twilio_channel', type: :r
|
||||
headers: admin.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
|
||||
expect(json_response['name']).to eq('SMS Channel')
|
||||
expect(json_response['phone_number']).to eq('+1234567890')
|
||||
@@ -124,7 +124,7 @@ RSpec.describe '/api/v1/accounts/{account.id}/channels/twilio_channel', type: :r
|
||||
headers: admin.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
|
||||
expect(json_response['phone_number']).to eq('whatsapp:+1224466880')
|
||||
end
|
||||
|
||||
@@ -33,7 +33,7 @@ RSpec.describe '/api/v1/accounts/{account.id}/contacts/:id/conversations', type:
|
||||
get "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/conversations", headers: admin.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
|
||||
expect(json_response['payload'].length).to eq 4
|
||||
end
|
||||
@@ -44,7 +44,7 @@ RSpec.describe '/api/v1/accounts/{account.id}/contacts/:id/conversations', type:
|
||||
get "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/conversations", headers: agent.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
|
||||
expect(json_response['payload'].length).to eq 2
|
||||
end
|
||||
@@ -55,7 +55,7 @@ RSpec.describe '/api/v1/accounts/{account.id}/contacts/:id/conversations', type:
|
||||
get "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/conversations", headers: unknown.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
|
||||
expect(json_response['payload'].length).to eq 0
|
||||
end
|
||||
|
||||
@@ -114,7 +114,7 @@ RSpec.describe 'Notes API', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(::Note.exists?(note.id)).to be false
|
||||
expect(Note.exists?(note.id)).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -35,7 +35,7 @@ RSpec.describe 'Contacts API', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_body = JSON.parse(response.body)
|
||||
response_body = response.parsed_body
|
||||
expect(response_body['payload'].first['email']).to eq(contact.email)
|
||||
expect(response_body['payload'].first['contact_inboxes'].first['source_id']).to eq(contact_inbox.source_id)
|
||||
expect(response_body['payload'].first['contact_inboxes'].first['inbox']['name']).to eq(contact_inbox.inbox.name)
|
||||
@@ -47,7 +47,7 @@ RSpec.describe 'Contacts API', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_body = JSON.parse(response.body)
|
||||
response_body = response.parsed_body
|
||||
expect(response_body['payload'].first['email']).to eq(contact.email)
|
||||
expect(response_body['payload'].first['contact_inboxes'].blank?).to be(true)
|
||||
end
|
||||
@@ -58,7 +58,7 @@ RSpec.describe 'Contacts API', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_body = JSON.parse(response.body)
|
||||
response_body = response.parsed_body
|
||||
expect(response_body['payload'].last['id']).to eq(contact_4.id)
|
||||
expect(response_body['payload'].last['email']).to eq(contact_4.email)
|
||||
end
|
||||
@@ -69,7 +69,7 @@ RSpec.describe 'Contacts API', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_body = JSON.parse(response.body)
|
||||
response_body = response.parsed_body
|
||||
expect(response_body['payload'].first['email']).to eq(contact_1.email)
|
||||
expect(response_body['payload'].first['id']).to eq(contact_1.id)
|
||||
expect(response_body['payload'].last['email']).to eq(contact_4.email)
|
||||
@@ -81,7 +81,7 @@ RSpec.describe 'Contacts API', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_body = JSON.parse(response.body)
|
||||
response_body = response.parsed_body
|
||||
expect(response_body['payload'].first['email']).to eq(contact.email)
|
||||
expect(response_body['payload'].first['id']).to eq(contact.id)
|
||||
expect(response_body['payload'].last['email']).to eq(contact_4.email)
|
||||
@@ -94,7 +94,7 @@ RSpec.describe 'Contacts API', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_body = JSON.parse(response.body)
|
||||
response_body = response.parsed_body
|
||||
expect(response_body['payload'].first['conversations_count']).to eq(contact.conversations.count)
|
||||
expect(response_body['payload'].first['last_seen_at']).present?
|
||||
end
|
||||
@@ -110,7 +110,7 @@ RSpec.describe 'Contacts API', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_body = JSON.parse(response.body)
|
||||
response_body = response.parsed_body
|
||||
expect(response_body['meta']['count']).to eq(2)
|
||||
expect(response_body['payload'].pluck('email')).to include(contact_with_label1.email, contact_with_label2.email)
|
||||
end
|
||||
@@ -160,7 +160,7 @@ RSpec.describe 'Contacts API', type: :request do
|
||||
post "/api/v1/accounts/#{account.id}/contacts/import",
|
||||
headers: admin.create_new_auth_token
|
||||
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(json_response['error']).to eq('File is blank')
|
||||
@@ -191,7 +191,7 @@ RSpec.describe 'Contacts API', type: :request do
|
||||
end
|
||||
|
||||
it 'returns all contacts who are online' do
|
||||
allow(::OnlineStatusTracker).to receive(:get_available_contact_ids).and_return([contact.id])
|
||||
allow(OnlineStatusTracker).to receive(:get_available_contact_ids).and_return([contact.id])
|
||||
|
||||
get "/api/v1/accounts/#{account.id}/contacts/active",
|
||||
headers: admin.create_new_auth_token,
|
||||
@@ -353,7 +353,7 @@ RSpec.describe 'Contacts API', type: :request do
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
# only the inboxes which agent has access to are shown
|
||||
expect(JSON.parse(response.body)['payload'].pluck('inbox').pluck('id')).to eq([twilio_whatsapp_inbox.id])
|
||||
expect(response.parsed_body['payload'].pluck('inbox').pluck('id')).to eq([twilio_whatsapp_inbox.id])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -383,7 +383,7 @@ RSpec.describe 'Contacts API', type: :request do
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
# custom attributes are updated
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['payload']['contact']['custom_attributes']).to eq({ 'test' => 'test', 'test1' => 'test1' })
|
||||
end
|
||||
|
||||
@@ -395,7 +395,7 @@ RSpec.describe 'Contacts API', type: :request do
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['message']).to eq('Name is too long (maximum is 255 characters)')
|
||||
end
|
||||
|
||||
@@ -460,7 +460,7 @@ RSpec.describe 'Contacts API', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(JSON.parse(response.body)['attributes']).to include('email')
|
||||
expect(response.parsed_body['attributes']).to include('email')
|
||||
end
|
||||
|
||||
it 'prevents updating with an existing phone number' do
|
||||
@@ -472,7 +472,7 @@ RSpec.describe 'Contacts API', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(JSON.parse(response.body)['attributes']).to include('phone_number')
|
||||
expect(response.parsed_body['attributes']).to include('phone_number')
|
||||
end
|
||||
|
||||
it 'updates avatar' do
|
||||
@@ -517,7 +517,7 @@ RSpec.describe 'Contacts API', type: :request do
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
|
||||
it 'deletes the contact for administrator user' do
|
||||
allow(::OnlineStatusTracker).to receive(:get_presence).and_return(false)
|
||||
allow(OnlineStatusTracker).to receive(:get_presence).and_return(false)
|
||||
delete "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
|
||||
headers: admin.create_new_auth_token
|
||||
|
||||
@@ -530,7 +530,7 @@ RSpec.describe 'Contacts API', type: :request do
|
||||
end
|
||||
|
||||
it 'does not delete the contact if online' do
|
||||
allow(::OnlineStatusTracker).to receive(:get_presence).and_return(true)
|
||||
allow(OnlineStatusTracker).to receive(:get_presence).and_return(true)
|
||||
|
||||
delete "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
|
||||
headers: admin.create_new_auth_token
|
||||
@@ -591,7 +591,7 @@ RSpec.describe 'Contacts API', type: :request do
|
||||
context 'when it is an authenticated user' do
|
||||
before do
|
||||
create(:contact, account: account)
|
||||
contact.avatar.attach(io: File.open(Rails.root.join('spec/assets/avatar.png')), filename: 'avatar.png', content_type: 'image/png')
|
||||
contact.avatar.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
|
||||
end
|
||||
|
||||
it 'delete contact avatar' do
|
||||
|
||||
@@ -26,7 +26,7 @@ RSpec.describe '/api/v1/accounts/:account_id/conversations/:conversation_id/dire
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['content_type']).to eq('image/png')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -45,7 +45,7 @@ RSpec.describe 'Conversation Messages API', type: :request do
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
|
||||
expect(json_response['error']).to eq('Validation failed: Content is too long (maximum is 150000 characters)')
|
||||
end
|
||||
@@ -61,7 +61,7 @@ RSpec.describe 'Conversation Messages API', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_data = JSON.parse(response.body)
|
||||
response_data = response.parsed_body
|
||||
expect(response_data['content_attributes']['external_created_at']).to eq time_stamp
|
||||
expect(conversation.messages.count).to eq(1)
|
||||
expect(conversation.messages.last.sender_id).to eq(agent_bot.id)
|
||||
|
||||
@@ -765,7 +765,7 @@ RSpec.describe 'Conversations API', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_body = JSON.parse(response.body)
|
||||
response_body = response.parsed_body
|
||||
expect(response_body['payload'].first['file_type']).to eq('image')
|
||||
end
|
||||
|
||||
@@ -775,7 +775,7 @@ RSpec.describe 'Conversations API', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_body = JSON.parse(response.body)
|
||||
response_body = response.parsed_body
|
||||
expect(response_body['payload'].length).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -30,7 +30,7 @@ RSpec.describe 'CSAT Survey Responses API', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(JSON.parse(response.body).first['feedback_message']).to eq(csat_survey_response.feedback_message)
|
||||
expect(response.parsed_body.first['feedback_message']).to eq(csat_survey_response.feedback_message)
|
||||
end
|
||||
|
||||
it 'filters csat responses based on a date range' do
|
||||
@@ -43,7 +43,7 @@ RSpec.describe 'CSAT Survey Responses API', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_data = JSON.parse(response.body)
|
||||
response_data = response.parsed_body
|
||||
expect(response_data.pluck('id')).to include(csat_3_days_ago.id)
|
||||
expect(response_data.pluck('id')).not_to include(csat_10_days_ago.id)
|
||||
end
|
||||
@@ -63,7 +63,7 @@ RSpec.describe 'CSAT Survey Responses API', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_data = JSON.parse(response.body)
|
||||
response_data = response.parsed_body
|
||||
expect(response_data.size).to eq 2
|
||||
end
|
||||
|
||||
@@ -104,7 +104,7 @@ RSpec.describe 'CSAT Survey Responses API', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_data = JSON.parse(response.body)
|
||||
response_data = response.parsed_body
|
||||
expect(response_data['total_count']).to eq 1
|
||||
expect(response_data['total_sent_messages_count']).to eq 0
|
||||
expect(response_data['ratings_count']).to eq({ '1' => 1 })
|
||||
@@ -123,7 +123,7 @@ RSpec.describe 'CSAT Survey Responses API', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_data = JSON.parse(response.body)
|
||||
response_data = response.parsed_body
|
||||
expect(response_data['total_count']).to eq 1
|
||||
expect(response_data['total_sent_messages_count']).to eq 0
|
||||
expect(response_data['ratings_count']).to eq({ '1' => 1 })
|
||||
@@ -144,7 +144,7 @@ RSpec.describe 'CSAT Survey Responses API', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_data = JSON.parse(response.body)
|
||||
response_data = response.parsed_body
|
||||
expect(response_data['total_count']).to eq 2
|
||||
expect(response_data['total_sent_messages_count']).to eq 0
|
||||
expect(response_data['ratings_count']).to eq({ '1' => 2 })
|
||||
|
||||
@@ -23,7 +23,7 @@ RSpec.describe 'Custom Attribute Definitions API', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_body = JSON.parse(response.body)
|
||||
response_body = response.parsed_body
|
||||
|
||||
expect(response_body.count).to eq(2)
|
||||
expect(response_body.first['attribute_key']).to eq(custom_attribute_definition.attribute_key)
|
||||
@@ -86,7 +86,7 @@ RSpec.describe 'Custom Attribute Definitions API', type: :request do
|
||||
end.to change(CustomAttributeDefinition, :count).by(1)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['attribute_key']).to eq 'developer_id'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -22,7 +22,7 @@ RSpec.describe 'Custom Filters API', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_body = JSON.parse(response.body)
|
||||
response_body = response.parsed_body
|
||||
expect(response_body.first['name']).to eq(custom_filter.name)
|
||||
expect(response_body.first['query']).to eq(custom_filter.query)
|
||||
end
|
||||
@@ -74,7 +74,7 @@ RSpec.describe 'Custom Filters API', type: :request do
|
||||
end.to change(CustomFilter, :count).by(1)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['name']).to eq 'vip-customers'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -22,7 +22,7 @@ RSpec.describe 'DashboardAppsController', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_body = JSON.parse(response.body)
|
||||
response_body = response.parsed_body
|
||||
expect(response_body.first['title']).to eq(dashboard_app.title)
|
||||
expect(response_body.first['content']).to eq(dashboard_app.content)
|
||||
end
|
||||
@@ -76,7 +76,7 @@ RSpec.describe 'DashboardAppsController', type: :request do
|
||||
end.to change(DashboardApp, :count).by(1)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['title']).to eq 'CRM Dashboard'
|
||||
expect(json_response['content'][0]['link']).to eq payload[:dashboard_app][:content][0][:link]
|
||||
expect(json_response['content'][0]['type']).to eq payload[:dashboard_app][:content][0][:type]
|
||||
@@ -89,7 +89,7 @@ RSpec.describe 'DashboardAppsController', type: :request do
|
||||
end.not_to change(DashboardApp, :count)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['message']).to eq 'Content : Invalid data'
|
||||
end
|
||||
|
||||
@@ -126,7 +126,7 @@ RSpec.describe 'DashboardAppsController', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
json_response = response.parsed_body
|
||||
expect(dashboard_app.reload.title).to eq('CRM Dashboard')
|
||||
expect(json_response['content'][0]['link']).to eq payload[:dashboard_app][:content][0][:link]
|
||||
expect(json_response['content'][0]['type']).to eq payload[:dashboard_app][:content][0][:type]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user