Merge branch 'feat/CW-3059' into feat/CW-3067-snooze
This commit is contained in:
@@ -8,8 +8,8 @@
|
||||
>
|
||||
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
|
||||
<template v-if="currentAccountId">
|
||||
<pending-email-verification-banner />
|
||||
<payment-pending-banner />
|
||||
<pending-email-verification-banner v-if="hideOnOnboardingView" />
|
||||
<payment-pending-banner v-if="hideOnOnboardingView" />
|
||||
<upgrade-banner />
|
||||
</template>
|
||||
<transition name="fade" mode="out-in">
|
||||
@@ -38,6 +38,7 @@ import vueActionCable from './helper/actionCable';
|
||||
import WootSnackbarBox from './components/SnackbarContainer.vue';
|
||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||
import { setColorTheme } from './helper/themeHelper';
|
||||
import { isOnOnboardingView } from 'v3/helpers/RouteHelper';
|
||||
import {
|
||||
registerSubscription,
|
||||
verifyServiceWorkerExistence,
|
||||
@@ -79,6 +80,9 @@ export default {
|
||||
const { accounts = [] } = this.currentUser || {};
|
||||
return accounts.length > 0;
|
||||
},
|
||||
hideOnOnboardingView() {
|
||||
return !isOnOnboardingView(this.$route);
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
||||
@@ -48,3 +48,13 @@ export const validateRouteAccess = (to, next, chatwootConfig = {}) => {
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
export const isOnOnboardingView = route => {
|
||||
const { name = '' } = route || {};
|
||||
|
||||
if (!name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return name.includes('onboarding_');
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { validateRouteAccess } from '../RouteHelper';
|
||||
import { validateRouteAccess, isOnOnboardingView } from '../RouteHelper';
|
||||
import { clearBrowserSessionCookies } from 'dashboard/store/utils/api';
|
||||
import { replaceRouteWithReload } from '../CommonHelper';
|
||||
import Cookies from 'js-cookie';
|
||||
@@ -67,3 +67,24 @@ describe('#validateRouteAccess', () => {
|
||||
expect(next).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isOnOnboardingView', () => {
|
||||
test('returns true for a route with onboarding name', () => {
|
||||
const route = { name: 'onboarding_welcome' };
|
||||
expect(isOnOnboardingView(route)).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false for a route without onboarding name', () => {
|
||||
const route = { name: 'home' };
|
||||
expect(isOnOnboardingView(route)).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false for a route with null name', () => {
|
||||
const route = { name: null };
|
||||
expect(isOnOnboardingView(route)).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false for an undefined route object', () => {
|
||||
expect(isOnOnboardingView()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,10 +37,24 @@ class ApplicationMailbox < ActionMailbox::Base
|
||||
# checks if follow this pattern send it to reply_mailbox
|
||||
# reply+<conversation-uuid>@<mailer-domain.com>
|
||||
def reply_uuid_mail?(inbound_mail)
|
||||
validate_to_address(inbound_mail)
|
||||
|
||||
inbound_mail.mail.to&.any? do |email|
|
||||
conversation_uuid = email.split('@')[0]
|
||||
conversation_uuid.match?(REPLY_EMAIL_UUID_PATTERN)
|
||||
end
|
||||
end
|
||||
|
||||
# if mail.to returns a string, then it is a malformed `to` header
|
||||
# valid `to` header will be of type Mail::AddressContainer
|
||||
# validate if the to address is of type string
|
||||
def validate_to_address(inbound_mail)
|
||||
to_address_class = inbound_mail.mail.to&.class
|
||||
|
||||
return if to_address_class == Mail::AddressContainer
|
||||
|
||||
Rails.logger.error "Email to address header is malformed `#{inbound_mail.mail.to}`"
|
||||
raise StandardError, "Invalid email to address header #{inbound_mail.mail.to}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,6 +3,7 @@ class ActionService
|
||||
|
||||
def initialize(conversation)
|
||||
@conversation = conversation.reload
|
||||
@account = @conversation.account
|
||||
end
|
||||
|
||||
def mute_conversation(_params)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
class AddUniqueIndexToAppliedSlas < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_index :applied_slas,
|
||||
[:account_id, :sla_policy_id, :conversation_id],
|
||||
unique: true,
|
||||
name: 'index_applied_slas_on_account_sla_policy_conversation'
|
||||
end
|
||||
end
|
||||
+2
-1
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.0].define(version: 2024_02_15_065844) do
|
||||
ActiveRecord::Schema[7.0].define(version: 2024_02_16_055809) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_stat_statements"
|
||||
enable_extension "pg_trgm"
|
||||
@@ -122,6 +122,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_02_15_065844) do
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "sla_status", default: 0
|
||||
t.index ["account_id", "sla_policy_id", "conversation_id"], name: "index_applied_slas_on_account_sla_policy_conversation", unique: true
|
||||
t.index ["account_id"], name: "index_applied_slas_on_account_id"
|
||||
t.index ["conversation_id"], name: "index_applied_slas_on_conversation_id"
|
||||
t.index ["sla_policy_id"], name: "index_applied_slas_on_sla_policy_id"
|
||||
|
||||
@@ -2,7 +2,7 @@ class Sla::TriggerSlasForAccountsJob < ApplicationJob
|
||||
queue_as :scheduled_jobs
|
||||
|
||||
def perform
|
||||
Account.find_each do |account|
|
||||
Account.joins(:sla_policies).distinct.find_each do |account|
|
||||
Rails.logger.info "Enqueuing ProcessAccountAppliedSlasJob for account #{account.id}"
|
||||
Sla::ProcessAccountAppliedSlasJob.perform_later(account)
|
||||
end
|
||||
|
||||
@@ -12,14 +12,17 @@
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_applied_slas_on_account_id (account_id)
|
||||
# index_applied_slas_on_conversation_id (conversation_id)
|
||||
# index_applied_slas_on_sla_policy_id (sla_policy_id)
|
||||
# index_applied_slas_on_account_id (account_id)
|
||||
# index_applied_slas_on_account_sla_policy_conversation (account_id,sla_policy_id,conversation_id) UNIQUE
|
||||
# index_applied_slas_on_conversation_id (conversation_id)
|
||||
# index_applied_slas_on_sla_policy_id (sla_policy_id)
|
||||
#
|
||||
class AppliedSla < ApplicationRecord
|
||||
belongs_to :account
|
||||
belongs_to :sla_policy
|
||||
belongs_to :conversation
|
||||
|
||||
validates :account_id, uniqueness: { scope: %i[sla_policy_id conversation_id] }
|
||||
|
||||
enum sla_status: { active: 0, hit: 1, missed: 2 }
|
||||
end
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
module Enterprise::ActionService
|
||||
def add_sla(sla_policy)
|
||||
def add_sla(sla_policy_id)
|
||||
return if sla_policy_id.blank?
|
||||
|
||||
sla_policy = @account.sla_policies.find_by(id: sla_policy_id.first)
|
||||
return if sla_policy.nil?
|
||||
return if @conversation.sla_policy.present?
|
||||
|
||||
Rails.logger.info "SLA:: Adding SLA #{sla_policy.id} to conversation: #{@conversation.id}"
|
||||
@conversation.update!(sla_policy_id: sla_policy.id)
|
||||
create_applied_sla(sla_policy)
|
||||
end
|
||||
|
||||
def create_applied_sla(sla_policy)
|
||||
Rails.logger.info "SLA:: Creating Applied SLA for conversation: #{@conversation.id}"
|
||||
AppliedSla.create!(
|
||||
account_id: @conversation.account_id,
|
||||
sla_policy_id: sla_policy.id,
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Sla::TriggerSlasForAccountsJob do
|
||||
context 'when perform is called' do
|
||||
let(:account) { create(:account) }
|
||||
let(:account_with_sla) { create(:account) }
|
||||
let(:account_without_sla) { create(:account) }
|
||||
|
||||
before do
|
||||
create(:sla_policy, account: account_with_sla)
|
||||
end
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { described_class.perform_later }.to have_enqueued_job(described_class)
|
||||
.on_queue('scheduled_jobs')
|
||||
end
|
||||
|
||||
it 'calls the ProcessAccountAppliedSlasJob' do
|
||||
expect(Sla::ProcessAccountAppliedSlasJob).to receive(:perform_later).with(account).and_call_original
|
||||
it 'calls the ProcessAccountAppliedSlasJob for accounts with SLA' do
|
||||
expect(Sla::ProcessAccountAppliedSlasJob).to receive(:perform_later).with(account_with_sla).and_call_original
|
||||
described_class.perform_now
|
||||
end
|
||||
|
||||
it 'does not call the ProcessAccountAppliedSlasJob for accounts without SLA' do
|
||||
expect(Sla::ProcessAccountAppliedSlasJob).not_to receive(:perform_later).with(account_without_sla)
|
||||
described_class.perform_now
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,16 +8,42 @@ describe ActionService do
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
let(:action_service) { described_class.new(conversation) }
|
||||
|
||||
it 'adds the sla policy to the conversation and create applied_sla entry' do
|
||||
action_service.add_sla(sla_policy)
|
||||
expect(conversation.reload.sla_policy_id).to eq(sla_policy.id)
|
||||
context 'when sla_policy_id is present' do
|
||||
it 'adds the sla policy to the conversation and create applied_sla entry' do
|
||||
action_service.add_sla([sla_policy.id])
|
||||
expect(conversation.reload.sla_policy_id).to eq(sla_policy.id)
|
||||
|
||||
# check if appliedsla table entry is created with matching attributes
|
||||
applied_sla = AppliedSla.last
|
||||
expect(applied_sla.account_id).to eq(account.id)
|
||||
expect(applied_sla.sla_policy_id).to eq(sla_policy.id)
|
||||
expect(applied_sla.conversation_id).to eq(conversation.id)
|
||||
expect(applied_sla.sla_status).to eq('active')
|
||||
# check if appliedsla table entry is created with matching attributes
|
||||
applied_sla = AppliedSla.last
|
||||
expect(applied_sla.account_id).to eq(account.id)
|
||||
expect(applied_sla.sla_policy_id).to eq(sla_policy.id)
|
||||
expect(applied_sla.conversation_id).to eq(conversation.id)
|
||||
expect(applied_sla.sla_status).to eq('active')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sla_policy_id is not present' do
|
||||
it 'does not add the sla policy to the conversation' do
|
||||
action_service.add_sla(nil)
|
||||
expect(conversation.reload.sla_policy_id).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation already has a sla policy' do
|
||||
it 'does not add the new sla policy to the conversation' do
|
||||
existing_sla_policy = sla_policy
|
||||
new_sla_policy = create(:sla_policy, account: account)
|
||||
conversation.update!(sla_policy_id: existing_sla_policy.id)
|
||||
action_service.add_sla([new_sla_policy.id])
|
||||
expect(conversation.reload.sla_policy_id).to eq(existing_sla_policy.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sla_policy is not found' do
|
||||
it 'does not add the sla policy to the conversation' do
|
||||
action_service.add_sla([sla_policy.id + 1])
|
||||
expect(conversation.reload.sla_policy_id).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
X-Original-To: bd84c730a1ac7833e4d27253804516f7@reply.chatwoot.com
|
||||
Received: from mail.planetmars.com (mxd [192.168.1.1]) by mx.sendgrid.net with ESMTP id AAAA-bCCCCCC5DeeeFFgg for <bd84c730a1ac7833e4d27253804516f7@reply.chatwoot.com>; Sun, 31 Dec 2023 22:32:23.586 +0000 (UTC)
|
||||
From: "Mark Whatney" <mark@planetmars.com>
|
||||
To: <vishnu@chatwoot.com>vishnu@chatwoot.com
|
||||
Subject: stranded in mars
|
||||
Date: Mon, 1 Jan 2024 06:31:44 +0800
|
||||
Message-ID: <1234560e0123c05b4bbf83c828b1688a93c7@com>
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/alternative;
|
||||
boundary=15688136a4ad411d82b004fae6e46549
|
||||
X-Exim-Id: 1234560e0123c05b4bbf83c828b1688a93c7
|
||||
|
||||
This is a multipart message in MIME format.
|
||||
|
||||
--15688136a4ad411d82b004fae6e46549
|
||||
Content-Type: text/plain;
|
||||
charset="us-ascii"
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
hey v, can i get some help over here?
|
||||
|
||||
--15688136a4ad411d82b004fae6e46549
|
||||
Content-Type: text/html;
|
||||
charset="utf-8"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
hey v, can i get some help over here?
|
||||
|
||||
--15688136a4ad411d82b004fae6e46549--
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
X-Original-To: bd84c730a1ac7833e4d27253804516f7@reply.chatwoot.com
|
||||
Received: from mail.planetmars.com (mxd [192.168.1.1]) by mx.sendgrid.net with ESMTP id AAAA-bCCCCCC5DeeeFFgg for <bd84c730a1ac7833e4d27253804516f7@reply.chatwoot.com>; Sun, 31 Dec 2023 22:32:23.586 +0000 (UTC)
|
||||
From: "Mark Whatney" <mark@planetmars.com>
|
||||
To: vishnu@chatwoot.com www.chatwoot.com
|
||||
Subject: stranded in mars
|
||||
Date: Mon, 1 Jan 2024 06:31:44 +0800
|
||||
Message-ID: <1234560e0123c05b4bbf83c828b1688a93c7@com>
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/alternative;
|
||||
boundary=15688136a4ad411d82b004fae6e46549
|
||||
X-Exim-Id: 1234560e0123c05b4bbf83c828b1688a93c7
|
||||
|
||||
This is a multipart message in MIME format.
|
||||
|
||||
--15688136a4ad411d82b004fae6e46549
|
||||
Content-Type: text/plain;
|
||||
charset="us-ascii"
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
hey v, can i get some help over here?
|
||||
|
||||
--15688136a4ad411d82b004fae6e46549
|
||||
Content-Type: text/html;
|
||||
charset="utf-8"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
hey v, can i get some help over here?
|
||||
|
||||
--15688136a4ad411d82b004fae6e46549--
|
||||
@@ -10,6 +10,8 @@ RSpec.describe ApplicationMailbox do
|
||||
let(:reply_mail_without_uuid) { create_inbound_email_from_fixture('reply.eml') }
|
||||
let(:reply_mail_with_in_reply_to) { create_inbound_email_from_fixture('in_reply_to.eml') }
|
||||
let(:support_mail) { create_inbound_email_from_fixture('support.eml') }
|
||||
let(:mail_with_invalid_to_address) { create_inbound_email_from_fixture('mail_with_invalid_to.eml') }
|
||||
let(:mail_with_invalid_to_address_2) { create_inbound_email_from_fixture('mail_with_invalid_to_2.eml') }
|
||||
|
||||
describe 'Default' do
|
||||
it 'catchall mails route to Default Mailbox' do
|
||||
@@ -65,5 +67,21 @@ RSpec.describe ApplicationMailbox do
|
||||
described_class.route reply_cc_mail
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Invalid Mail To Address' do
|
||||
it 'raises error when mail.to header is malformed' do
|
||||
expect do
|
||||
described_class.route mail_with_invalid_to_address
|
||||
end.to raise_error(StandardError,
|
||||
'Invalid email to address header <vishnu@chatwoot.com>vishnu@chatwoot.com')
|
||||
end
|
||||
|
||||
it 'raises another error when mail.to header is malformed' do
|
||||
expect do
|
||||
described_class.route mail_with_invalid_to_address_2
|
||||
end.to raise_error(StandardError,
|
||||
'Invalid email to address header vishnu@chatwoot.com www.chatwoot.com')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12597,14 +12597,14 @@ ip-regex@^2.1.0:
|
||||
integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=
|
||||
|
||||
ip@^1.1.0, ip@^1.1.5:
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
|
||||
integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=
|
||||
version "1.1.9"
|
||||
resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.9.tgz#8dfbcc99a754d07f425310b86a99546b1151e396"
|
||||
integrity sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==
|
||||
|
||||
ip@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da"
|
||||
integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.1.tgz#e8f3595d33a3ea66490204234b77636965307105"
|
||||
integrity sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==
|
||||
|
||||
ipaddr.js@1.9.1, ipaddr.js@^1.9.0:
|
||||
version "1.9.1"
|
||||
|
||||
Reference in New Issue
Block a user