Compare commits

..
23 changed files with 41 additions and 291 deletions
-1
View File
@@ -17,7 +17,6 @@ gem 'jbuilder'
gem 'kaminari'
gem 'responders'
gem 'rest-client'
gem 'scenic'
gem 'telephone_number'
gem 'time_diff'
gem 'tzinfo-data'
-4
View File
@@ -656,9 +656,6 @@ GEM
sprockets (> 3.0)
sprockets-rails
tilt
scenic (1.7.0)
activerecord (>= 4.0.0)
railties (>= 4.0.0)
scout_apm (5.3.3)
parser
scss_lint (0.60.0)
@@ -884,7 +881,6 @@ DEPENDENCIES
rubocop-performance
rubocop-rails
rubocop-rspec
scenic
scout_apm
scss_lint
seed_dump
+3 -3
View File
@@ -155,9 +155,9 @@ class ConversationFinder
end
def conversations
@conversations = @conversations.includes(:taggings, :inbox,
{ assignee: [{ account_users: [:account] }, { avatar_attachment: [:blob] }] },
{ contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox, :messages)
@conversations = @conversations.includes(
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox
)
sort_by = SORT_OPTIONS[params[:sort_by]] || SORT_OPTIONS['latest']
@conversations.send(sort_by).page(current_page)
end
@@ -115,12 +115,10 @@
</template>
<script>
import format from 'date-fns/format';
import { required, url } from 'vuelidate/lib/validators';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
import { isValidURL } from '../helper/URLHelper';
const DATE_FORMAT = 'yyyy-MM-dd';
export default {
components: {
@@ -144,9 +142,6 @@ export default {
computed: {
formattedValue() {
if (this.isAttributeTypeDate) {
return format(new Date(this.value || new Date()), DATE_FORMAT);
}
if (this.isAttributeTypeCheckbox) {
return this.value === 'false' ? false : this.value;
}
@@ -193,8 +188,11 @@ export default {
return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.REQUIRED');
},
displayValue() {
if (this.attributeType === 'date') {
return format(new Date(this.editedValue), 'dd-MM-yyyy');
if (this.attributeType === 'date' && this.editedValue) {
return this.editedValue
.split('-')
.reverse()
.join('-');
}
return this.editedValue;
},
@@ -247,17 +245,12 @@ export default {
}
},
onUpdate() {
const updatedValue =
this.attributeType === 'date'
? format(new Date(this.editedValue), DATE_FORMAT)
: this.editedValue;
this.$v.$touch();
if (this.$v.$invalid) {
return;
}
this.isEditing = false;
this.$emit('update', this.attributeKey, updatedValue);
this.$emit('update', this.attributeKey, this.editedValue);
},
onDelete() {
this.isEditing = false;
+2 -16
View File
@@ -35,9 +35,9 @@ class Imap::ImapMailbox
end
def find_conversation_by_in_reply_to
return if in_reply_to.blank? && @inbound_mail.references.blank?
return if in_reply_to.blank?
message = @inbox.messages.find_by(source_id: in_reply_to) || find_message_by_references
message = @inbox.messages.find_by(source_id: in_reply_to)
if message.nil?
@inbox.conversations.where("additional_attributes->>'in_reply_to' = ?", in_reply_to).first
else
@@ -49,20 +49,6 @@ class Imap::ImapMailbox
@inbound_mail.in_reply_to
end
def find_message_by_references
message_to_return = nil
return if @inbound_mail.references.blank?
references = @inbound_mail.references
references.each do |message_id|
message = @inbox.messages.find_by(source_id: message_id)
message_to_return = message if message.present?
end
message_to_return
end
def find_or_create_conversation
@conversation = find_conversation_by_in_reply_to || ::Conversation.create!({ account_id: @account.id,
inbox_id: @inbox.id,
-1
View File
@@ -28,7 +28,6 @@ class AgentBot < ApplicationRecord
enum bot_type: { webhook: 0, csml: 1 }
validate :validate_agent_bot_config
validates :outgoing_url, length: { maximum: Limits::URL_LENGTH_LIMIT }
def available_name
name
+1 -1
View File
@@ -37,7 +37,7 @@ class Attachment < ApplicationRecord
belongs_to :message
has_one_attached :file
validate :acceptable_file
validates :external_url, length: { maximum: Limits::URL_LENGTH_LIMIT }
validates :external_url, length: { maximum: 1000 }
enum file_type: [:image, :audio, :video, :file, :location, :fallback, :share, :story_mention, :contact]
def push_event_data
-1
View File
@@ -27,7 +27,6 @@ class Channel::Api < ApplicationRecord
has_secure_token :identifier
has_secure_token :hmac_token
validate :ensure_valid_agent_reply_time_window
validates :webhook_url, length: { maximum: Limits::URL_LENGTH_LIMIT }
def name
'API'
+1 -1
View File
@@ -19,7 +19,7 @@
# index_contacts_on_account_id (account_id)
# index_contacts_on_lower_email_account_id (lower((email)::text), account_id)
# index_contacts_on_name_email_phone_number_identifier (name,email,phone_number,identifier) USING gin
# index_contacts_on_nonempty_fields (account_id,email,phone_number,identifier) WHERE (((email)::text <> ''::text) OR ((phone_number)::text <> ''::text) OR ((identifier)::text <> ''::text)) # rubocop:disable Layout/LineLength
# index_contacts_on_identifiable_fields (account_id,email,phone_number,identifier) WHERE (((email)::text <> ''::text) OR ((phone_number)::text <> ''::text) OR ((identifier)::text <> ''::text)) # rubocop:disable Layout/LineLength
# index_contacts_on_phone_number_and_account_id (phone_number,account_id)
# uniq_email_per_account_contact (email,account_id) UNIQUE
# uniq_identifier_per_account_contact (identifier,account_id) UNIQUE
-84
View File
@@ -1,84 +0,0 @@
# == Schema Information
#
# Table name: conversation_with_labels
#
# id :integer
# additional_attributes :jsonb
# agent_last_seen_at :datetime
# assignee_last_seen_at :datetime
# contact_last_seen_at :datetime
# custom_attributes :jsonb
# first_reply_created_at :datetime
# identifier :string
# labels_array :string is an Array
# last_activity_at :datetime
# priority :integer
# snoozed_until :datetime
# status :integer
# uuid :uuid
# created_at :datetime
# updated_at :datetime
# account_id :integer
# assignee_id :integer
# campaign_id :bigint
# contact_id :bigint
# contact_inbox_id :bigint
# display_id :integer
# inbox_id :integer
# team_id :bigint
#
# Indexes
#
# idx_conv_labels_view__acc_id__status__last_activity (account_id,status,last_activity_at)
# index_conversation_with_labels_on_custom_attributes (custom_attributes) USING gin
# index_conversation_with_labels_on_id (id) UNIQUE
# index_conversation_with_labels_on_labels_array (labels_array) USING gin
# index_conversation_with_labels_on_last_activity_at (last_activity_at)
#
class ConversationWithLabel < ApplicationRecord
self.primary_key = :id
belongs_to :account
belongs_to :inbox
belongs_to :assignee, class_name: 'User', optional: true
belongs_to :contact
belongs_to :contact_inbox
belongs_to :team, optional: true
belongs_to :campaign, optional: true
has_many :mentions
has_many :messages
has_one :csat_survey_response
has_many :conversation_participants
has_many :notifications, as: :primary_actor
has_many :attachments, through: :messages
scope :assigned_to, ->(agent) { where(assignee_id: agent.id) }
scope :unassigned, -> { where(assignee_id: nil) }
scope :assigned, -> { where.not(assignee_id: nil) }
scope :latest, -> { order(last_activity_at: :desc) }
def self.refresh
# https://github.com/scenic-views/scenic#what-about-materialized-views
# needs atleast one unuque index for concurrent refresh
Scenic.database.refresh_materialized_view(table_name, concurrently: true, cascade: false)
end
def label_list
labels_array
end
def muted?
Redis::Alfred.get(mute_key).present?
end
def readonly?
true
end
private
def mute_key
format(Redis::RedisKeys::CONVERSATION_MUTE_KEY, id: id)
end
end
+5 -5
View File
@@ -36,20 +36,20 @@ class Conversations::FilterService < FilterService
case current_filter['attribute_type']
when 'additional_attributes'
" additional_attributes ->> '#{attribute_key}' #{filter_operator_value} #{query_operator} "
" conversations.additional_attributes ->> '#{attribute_key}' #{filter_operator_value} #{query_operator} "
when 'date_attributes'
" (#{attribute_key})::#{current_filter['data_type']} #{filter_operator_value}#{current_filter['data_type']} #{query_operator} "
" (conversations.#{attribute_key})::#{current_filter['data_type']} #{filter_operator_value}#{current_filter['data_type']} #{query_operator} "
when 'standard'
if attribute_key == 'labels'
" tags.name #{filter_operator_value} #{query_operator} "
else
" #{attribute_key} #{filter_operator_value} #{query_operator} "
" conversations.#{attribute_key} #{filter_operator_value} #{query_operator} "
end
end
end
def base_relation
ConversationWithLabel.where(account_id: Current.account.id)
Current.account.conversations.left_outer_joins(:labels)
end
def current_page
@@ -58,7 +58,7 @@ class Conversations::FilterService < FilterService
def conversations
@conversations = @conversations.includes(
:inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :messages, :contact_inbox
)
@conversations.latest.page(current_page)
@@ -16,20 +16,20 @@ json.meta do
json.hmac_verified conversation.contact_inbox&.hmac_verified
end
# json.id conversation.display_id
# if conversation.messages.first.blank?
# json.messages []
# elsif conversation.unread_incoming_messages.count.zero?
# json.messages [conversation.messages.includes([{ attachments: [{ file_attachment: [:blob] }] }]).last.try(:push_event_data)]
# else
# json.messages conversation.unread_messages.includes([{ attachments: [{ file_attachment: [:blob] }] }]).last(10).map(&:push_event_data)
# end
json.id conversation.display_id
if conversation.messages.first.blank?
json.messages []
elsif conversation.unread_incoming_messages.count.zero?
json.messages [conversation.messages.includes([{ attachments: [{ file_attachment: [:blob] }] }]).last.try(:push_event_data)]
else
json.messages conversation.unread_messages.includes([:user, { attachments: [{ file_attachment: [:blob] }] }]).last(10).map(&:push_event_data)
end
json.account_id conversation.account_id
json.additional_attributes conversation.additional_attributes
json.agent_last_seen_at conversation.agent_last_seen_at.to_i
json.assignee_last_seen_at conversation.assignee_last_seen_at.to_i
# json.can_reply conversation.can_reply?
json.can_reply conversation.can_reply?
json.contact_last_seen_at conversation.contact_last_seen_at.to_i
json.custom_attributes conversation.custom_attributes
json.inbox_id conversation.inbox_id
@@ -39,8 +39,8 @@ json.snoozed_until conversation.snoozed_until
json.status conversation.status
json.created_at conversation.created_at.to_i
json.timestamp conversation.last_activity_at.to_i
# json.first_reply_created_at conversation.first_reply_created_at.to_i
# json.unread_count conversation.unread_incoming_messages.count
# json.last_non_activity_message conversation.messages.non_activity_messages.first.try(:push_event_data)
json.first_reply_created_at conversation.first_reply_created_at.to_i
json.unread_count conversation.unread_incoming_messages.count
json.last_non_activity_message conversation.messages.non_activity_messages.first.try(:push_event_data)
json.last_activity_at conversation.last_activity_at.to_i
json.priority conversation.priority
@@ -1,5 +0,0 @@
class CreateConversationWithLabels < ActiveRecord::Migration[7.0]
def change
create_view :conversation_with_labels, materialized: true
end
end
@@ -1,13 +0,0 @@
class AddIndexToConversationWithLabelsView < ActiveRecord::Migration[7.0]
disable_ddl_transaction!
def change
# needs atleast one unuque index for concurrent refresh
add_index :conversation_with_labels, :id, unique: true, algorithm: :concurrently
add_index :conversation_with_labels, [:account_id, :status, :last_activity_at], algorithm: :concurrently,
name: 'idx_conv_labels_view__acc_id__status__last_activity'
add_index :conversation_with_labels, :custom_attributes, using: :gin, algorithm: :concurrently
add_index :conversation_with_labels, :labels_array, using: :gin, algorithm: :concurrently
add_index :conversation_with_labels, :last_activity_at, order: { last_activity_at: :desc }, algorithm: :concurrently
end
end
+1 -38
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2023_06_02_123133) do
ActiveRecord::Schema[7.0].define(version: 2023_05_23_104139) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
enable_extension "pg_trgm"
@@ -935,43 +935,6 @@ ActiveRecord::Schema[7.0].define(version: 2023_06_02_123133) do
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "inboxes", "portals"
create_view "conversation_with_labels", materialized: true, sql_definition: <<-SQL
SELECT conversations.id,
conversations.account_id,
conversations.inbox_id,
conversations.status,
conversations.assignee_id,
conversations.created_at,
conversations.updated_at,
conversations.contact_id,
conversations.display_id,
conversations.contact_last_seen_at,
conversations.agent_last_seen_at,
conversations.additional_attributes,
conversations.contact_inbox_id,
conversations.uuid,
conversations.identifier,
conversations.last_activity_at,
conversations.team_id,
conversations.campaign_id,
conversations.snoozed_until,
conversations.custom_attributes,
conversations.assignee_last_seen_at,
conversations.first_reply_created_at,
conversations.priority,
array_agg(tags.name) AS labels_array
FROM ((conversations
LEFT JOIN taggings ON (((taggings.taggable_id = conversations.id) AND ((taggings.taggable_type)::text = 'Conversation'::text) AND ((taggings.context)::text = 'labels'::text))))
LEFT JOIN tags ON ((tags.id = taggings.tag_id)))
GROUP BY conversations.id;
SQL
add_index "conversation_with_labels", ["account_id", "status", "last_activity_at"], name: "idx_conv_labels_view__acc_id__status__last_activity"
add_index "conversation_with_labels", ["custom_attributes"], name: "index_conversation_with_labels_on_custom_attributes", using: :gin
add_index "conversation_with_labels", ["id"], name: "index_conversation_with_labels_on_id", unique: true
add_index "conversation_with_labels", ["labels_array"], name: "index_conversation_with_labels_on_labels_array", using: :gin
add_index "conversation_with_labels", ["last_activity_at"], name: "index_conversation_with_labels_on_last_activity_at", order: :desc
create_trigger("accounts_after_insert_row_tr", :generated => true, :compatibility => 1).
on("accounts").
after(:insert).
-11
View File
@@ -1,11 +0,0 @@
SELECT
conversations.*,
array_agg(tags.name) as labels_array
FROM
conversations
LEFT JOIN taggings ON taggings.taggable_id = conversations.id
AND taggings.taggable_type = 'Conversation'
AND taggings.context = 'labels'
LEFT JOIN tags ON tags.id = taggings.tag_id
GROUP BY
conversations.id;
-1
View File
@@ -1,5 +1,4 @@
module Limits
BULK_ACTIONS_LIMIT = 100
BULK_EXTERNAL_HTTP_CALLS_LIMIT = 25
URL_LENGTH_LIMIT = 2048 # https://stackoverflow.com/questions/417142
end
-16
View File
@@ -1,16 +0,0 @@
From: Sony Mathew <sony@chatwoot.com>
Mime-Version: 1.0 (Apple Message framework v1244.3)
Content-Type: multipart/alternative; boundary="Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74"
Subject: Discussion: Let's debate these attachments
Date: Tue, 20 Apr 2020 04:20:20 -0400
In-Reply-To: <4e6e35f5a38b4_479f13bb90078178@small-app-01.mail>
References: <4e6e35f5a38b4_479f13bb90078178@small-app-01.mail> <test-reference-id>
Message-Id: <0CB459E0-0336-41DA-BC88-E6E28C697DDBF@chatwoot.com>
X-Mailer: Apple Mail (2.1244.3)
--Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain;
charset=utf-8
References Email
-23
View File
@@ -83,34 +83,11 @@ RSpec.describe Imap::ImapMailbox do
let(:reply_mail) do
create_inbound_email_from_mail(from: 'email@gmail.com', to: 'imap@gmail.com', subject: 'Hello!', in_reply_to: 'test-in-reply-to')
end
let(:references_email) { create_inbound_email_from_fixture('references.eml') }
it 'creates new email conversation with incoming in-reply-to' do
class_instance.process(reply_mail.mail, channel)
expect(conversation.additional_attributes['in_reply_to']).to eq(reply_mail.mail.in_reply_to)
end
it 'append email to conversation with references id' do
inbox = Inbox.last
message = create(
:message,
content: 'Incoming Message',
message_type: 'incoming',
inbox: inbox,
source_id: 'test-reference-id',
account: account,
conversation: conversation
)
conversation = message.conversation
expect(conversation.messages.size).to eq(1)
class_instance.process(references_email.mail, inbox.channel)
expect(conversation.messages.size).to eq(2)
expect(conversation.messages.last.content).to eq('References Email')
expect(references_email.mail.references).to include('test-reference-id')
end
end
end
end
-15
View File
@@ -12,19 +12,4 @@ RSpec.describe AgentBot do
it_behaves_like 'access_tokenable'
it_behaves_like 'avatarable'
end
context 'when it validates outgoing_url length' do
let(:agent_bot) { create(:agent_bot) }
it 'valid when within limit' do
agent_bot.outgoing_url = 'a' * Limits::URL_LENGTH_LIMIT
expect(agent_bot.valid?).to be true
end
it 'invalid when crossed the limit' do
agent_bot.outgoing_url = 'a' * (Limits::URL_LENGTH_LIMIT + 1)
agent_bot.valid?
expect(agent_bot.errors[:outgoing_url]).to include("is too long (maximum is #{Limits::URL_LENGTH_LIMIT} characters)")
end
end
end
+3 -3
View File
@@ -11,14 +11,14 @@ RSpec.describe Attachment do
context 'when it validates external url length' do
it 'valid when within limit' do
attachment.external_url = 'a' * Limits::URL_LENGTH_LIMIT
attachment.external_url = 'a' * 1000
expect(attachment.valid?).to be true
end
it 'invalid when crossed the limit' do
attachment.external_url = 'a' * (Limits::URL_LENGTH_LIMIT + 5)
attachment.external_url = 'a' * 1500
attachment.valid?
expect(attachment.errors[:external_url]).to include("is too long (maximum is #{Limits::URL_LENGTH_LIMIT} characters)")
expect(attachment.errors[:external_url]).to include('is too long (maximum is 1000 characters)')
end
end
end
-23
View File
@@ -1,23 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Channel::Api do
# This validation happens in ApplicationRecord
describe 'length validations' do
let(:channel_api) { create(:channel_api) }
context 'when it validates webhook_url length' do
it 'valid when within limit' do
channel_api.webhook_url = 'a' * Limits::URL_LENGTH_LIMIT
expect(channel_api.valid?).to be true
end
it 'invalid when crossed the limit' do
channel_api.webhook_url = 'a' * (Limits::URL_LENGTH_LIMIT + 1)
channel_api.valid?
expect(channel_api.errors[:webhook_url]).to include("is too long (maximum is #{Limits::URL_LENGTH_LIMIT} characters)")
end
end
end
end
+7 -1
View File
@@ -2180,8 +2180,9 @@
"@chatwoot/prosemirror-schema@https://github.com/chatwoot/prosemirror-schema.git":
version "1.0.0"
resolved "https://github.com/chatwoot/prosemirror-schema.git#b937c3cb44210b7d251daf766988758975befec7"
resolved "https://github.com/chatwoot/prosemirror-schema.git#218f22b79d10f0a1240a125b0887b19186205ea6"
dependencies:
markdown-it-sup "^1.0.0"
prosemirror-commands "^1.1.4"
prosemirror-dropcursor "^1.3.2"
prosemirror-gapcursor "^1.1.5"
@@ -12292,6 +12293,11 @@ markdown-it-link-attributes@^4.0.1:
resolved "https://registry.yarnpkg.com/markdown-it-link-attributes/-/markdown-it-link-attributes-4.0.1.tgz#25751f2cf74fd91f0a35ba7b3247fa45f2056d88"
integrity sha512-pg5OK0jPLg62H4k7M9mRJLT61gUp9nvG0XveKYHMOOluASo9OEF13WlXrpAp2aj35LbedAy3QOCgQCw0tkLKAQ==
markdown-it-sup@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/markdown-it-sup/-/markdown-it-sup-1.0.0.tgz#cb9c9ff91a5255ac08f3fd3d63286e15df0a1fc3"
integrity sha512-E32m0nV9iyhRR7CrhnzL5msqic7rL1juWre6TQNxsnApg7Uf+F97JOKxUijg5YwXz86lZ0mqfOnutoryyNdntQ==
markdown-it@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-10.0.0.tgz#abfc64f141b1722d663402044e43927f1f50a8dc"