Merge branch 'release/3.10.2'
This commit is contained in:
4
Gemfile
4
Gemfile
@@ -111,9 +111,9 @@ gem 'elastic-apm', require: false
|
||||
gem 'newrelic_rpm', require: false
|
||||
gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
|
||||
gem 'scout_apm', require: false
|
||||
gem 'sentry-rails', '>= 5.14.0', require: false
|
||||
gem 'sentry-rails', '>= 5.18.0', require: false
|
||||
gem 'sentry-ruby', require: false
|
||||
gem 'sentry-sidekiq', '>= 5.15.0', require: false
|
||||
gem 'sentry-sidekiq', '>= 5.18.0', require: false
|
||||
|
||||
##-- background job processing --##
|
||||
gem 'sidekiq', '>= 7.2.4'
|
||||
|
||||
18
Gemfile.lock
18
Gemfile.lock
@@ -150,7 +150,7 @@ GEM
|
||||
statsd-ruby (~> 1.1)
|
||||
base64 (0.2.0)
|
||||
bcrypt (3.1.20)
|
||||
bigdecimal (3.1.7)
|
||||
bigdecimal (3.1.8)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.16.0)
|
||||
msgpack (~> 1.2)
|
||||
@@ -603,7 +603,7 @@ GEM
|
||||
ffi (~> 1.0)
|
||||
redis (5.0.6)
|
||||
redis-client (>= 0.9.0)
|
||||
redis-client (0.22.1)
|
||||
redis-client (0.22.2)
|
||||
connection_pool
|
||||
redis-namespace (1.10.0)
|
||||
redis (>= 4)
|
||||
@@ -703,14 +703,14 @@ GEM
|
||||
activesupport (>= 4)
|
||||
selectize-rails (0.12.6)
|
||||
semantic_range (3.0.0)
|
||||
sentry-rails (5.17.3)
|
||||
sentry-rails (5.18.0)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby (~> 5.17.3)
|
||||
sentry-ruby (5.17.3)
|
||||
sentry-ruby (~> 5.18.0)
|
||||
sentry-ruby (5.18.0)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sentry-sidekiq (5.17.3)
|
||||
sentry-ruby (~> 5.17.3)
|
||||
sentry-sidekiq (5.18.0)
|
||||
sentry-ruby (~> 5.18.0)
|
||||
sidekiq (>= 3.0)
|
||||
sexp_processor (4.17.0)
|
||||
shoulda-matchers (5.3.0)
|
||||
@@ -931,9 +931,9 @@ DEPENDENCIES
|
||||
scout_apm
|
||||
scss_lint
|
||||
seed_dump
|
||||
sentry-rails (>= 5.14.0)
|
||||
sentry-rails (>= 5.18.0)
|
||||
sentry-ruby
|
||||
sentry-sidekiq (>= 5.15.0)
|
||||
sentry-sidekiq (>= 5.18.0)
|
||||
shoulda-matchers
|
||||
sidekiq (>= 7.2.4)
|
||||
sidekiq-cron (>= 1.12.0)
|
||||
|
||||
@@ -121,7 +121,7 @@ export const generateConditionOptions = (options, key = 'id') => {
|
||||
};
|
||||
|
||||
// Add the "None" option to the agent list
|
||||
export const agentList = agents => [
|
||||
export const addNoneToList = agents => [
|
||||
{
|
||||
id: 'nil',
|
||||
name: 'None',
|
||||
@@ -137,8 +137,8 @@ export const getActionOptions = ({
|
||||
type,
|
||||
}) => {
|
||||
const actionsMap = {
|
||||
assign_agent: agentList(agents),
|
||||
assign_team: teams,
|
||||
assign_agent: addNoneToList(agents),
|
||||
assign_team: addNoneToList(teams),
|
||||
send_email_to_team: teams,
|
||||
add_label: generateConditionOptions(labels, 'title'),
|
||||
remove_label: generateConditionOptions(labels, 'title'),
|
||||
|
||||
@@ -39,6 +39,14 @@
|
||||
:readable-time="readableTime"
|
||||
@error="onImageLoadError"
|
||||
/>
|
||||
|
||||
<video-bubble
|
||||
v-if="attachment.file_type === 'video' && !hasVideoError"
|
||||
:url="attachment.data_url"
|
||||
:readable-time="readableTime"
|
||||
@error="onVideoLoadError"
|
||||
/>
|
||||
|
||||
<file-bubble
|
||||
v-else
|
||||
:url="attachment.data_url"
|
||||
@@ -72,6 +80,7 @@
|
||||
import UserMessageBubble from 'widget/components/UserMessageBubble.vue';
|
||||
import MessageReplyButton from 'widget/components/MessageReplyButton.vue';
|
||||
import ImageBubble from 'widget/components/ImageBubble.vue';
|
||||
import VideoBubble from 'widget/components/VideoBubble.vue';
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
import FileBubble from 'widget/components/FileBubble.vue';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
@@ -88,6 +97,7 @@ export default {
|
||||
UserMessageBubble,
|
||||
MessageReplyButton,
|
||||
ImageBubble,
|
||||
VideoBubble,
|
||||
FileBubble,
|
||||
FluentIcon,
|
||||
ReplyToChip,
|
||||
@@ -107,6 +117,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
hasImageError: false,
|
||||
hasVideoError: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -143,10 +154,12 @@ export default {
|
||||
watch: {
|
||||
message() {
|
||||
this.hasImageError = false;
|
||||
this.hasVideoError = false;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.hasImageError = false;
|
||||
this.hasVideoError = false;
|
||||
},
|
||||
methods: {
|
||||
async retrySendMessage() {
|
||||
@@ -158,6 +171,9 @@ export default {
|
||||
onImageLoadError() {
|
||||
this.hasImageError = true;
|
||||
},
|
||||
onVideoLoadError() {
|
||||
this.hasVideoError = true;
|
||||
},
|
||||
toggleReply() {
|
||||
this.$emitter.emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.message);
|
||||
},
|
||||
|
||||
27
app/javascript/widget/components/VideoBubble.vue
Normal file
27
app/javascript/widget/components/VideoBubble.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
url: { type: String, default: '' },
|
||||
readableTime: { type: String, default: '' },
|
||||
});
|
||||
|
||||
const emits = defineEmits(['error']);
|
||||
|
||||
const onVideoError = () => {
|
||||
emits('error');
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="block relative max-w-full">
|
||||
<video
|
||||
class="w-full max-w-[250px] h-auto"
|
||||
:src="url"
|
||||
controls
|
||||
@error="onVideoError"
|
||||
/>
|
||||
<span
|
||||
class="text-xs absolute text-white dark:text-white right-3 bottom-1 whitespace-nowrap"
|
||||
>
|
||||
{{ readableTime }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -48,7 +48,6 @@ class JsonSchemaValidator < ActiveModel::Validator
|
||||
|
||||
# Add validation errors to the record with a formatted statement
|
||||
validation_errors.each do |error|
|
||||
# byebug
|
||||
format_and_append_error(error, record)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -50,7 +50,11 @@ class ActionService
|
||||
end
|
||||
|
||||
def assign_team(team_ids = [])
|
||||
return unassign_team if team_ids[0]&.zero?
|
||||
# FIXME: The explicit checks for zero or nil (string) is bad. Move
|
||||
# this to a separate unassign action.
|
||||
should_unassign = team_ids.blank? || %w[nil 0].include?(team_ids[0].to_s)
|
||||
return @conversation.update!(team_id: nil) if should_unassign
|
||||
|
||||
# check if team belongs to account only if team_id is present
|
||||
# if team_id is nil, then it means that the team is being unassigned
|
||||
return unless !team_ids[0].nil? && team_belongs_to_account?(team_ids)
|
||||
|
||||
@@ -66,7 +66,7 @@ class Notification::PushNotificationService
|
||||
def send_browser_push(subscription)
|
||||
return unless can_send_browser_push?(subscription)
|
||||
|
||||
WebPush.payload_send(browser_push_payload(subscription))
|
||||
WebPush.payload_send(**browser_push_payload(subscription))
|
||||
Rails.logger.info("Browser push sent to #{user.email} with title #{push_message[:title]}")
|
||||
rescue WebPush::ExpiredSubscription, WebPush::InvalidSubscription, WebPush::Unauthorized => e
|
||||
Rails.logger.info "WebPush subscription expired: #{e.message}"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
shared: &shared
|
||||
version: '3.10.1'
|
||||
version: '3.10.2'
|
||||
|
||||
development:
|
||||
<<: *shared
|
||||
|
||||
@@ -31,7 +31,7 @@ vi:
|
||||
disposable_email: Chúng tôi không cho phép các email dùng một lần
|
||||
invalid_email: Bạn đã nhập một email không hợp lệ
|
||||
email_already_exists: "Bạn đã đăng ký một tài khoản với %{email}"
|
||||
invalid_params: 'Invalid, please check the signup paramters and try again'
|
||||
invalid_params: 'Không hợp lệ, vui lòng kiểm tra thông số đăng ký và thử lại'
|
||||
failed: Đăng ký thât bại
|
||||
data_import:
|
||||
data_type:
|
||||
@@ -51,7 +51,7 @@ vi:
|
||||
dyte:
|
||||
invalid_message_type: "Loại tin nhắn không hợp lệ. Hành động không được phép"
|
||||
slack:
|
||||
invalid_channel_id: "Invalid slack channel. Please try again"
|
||||
invalid_channel_id: "Kênh chùng không hợp lệ. Vui lòng thử lại"
|
||||
inboxes:
|
||||
imap:
|
||||
socket_error: Vui lòng kiểm tra kết nối mạng, địa chỉ IMAP và thử lại.
|
||||
@@ -63,43 +63,43 @@ vi:
|
||||
name: không nên bắt đầu hoặc kết thúc bằng các ký hiệu và không nên có kí tự < > / \ @.
|
||||
custom_filters:
|
||||
number_of_records: Đã đạt giới hạn. Số lượng tuỳ chọn lọc tối đa cho mỗi mỗi người dùng mỗi tài khoản là 50.
|
||||
invalid_attribute: Invalid attribute key - [%{key}]. The key should be one of [%{allowed_keys}] or a custom attribute defined in the account.
|
||||
invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}].
|
||||
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
|
||||
invalid_attribute: Khóa thuộc tính không hợp lệ - [%{key}]. Chìa khóa phải là một trong [%{allowed_keys}] hoặc thuộc tính tùy chỉnh được xác định trong tài khoản.
|
||||
invalid_operator: Toán tử không hợp lệ. Các toán tử được phép cho %{attribute_name} là [%{allowed_keys}].
|
||||
invalid_value: Giá trị không hợp lệ. Các giá trị được cung cấp cho %{attribute_name} không hợp lệ
|
||||
reports:
|
||||
period: Thời gian báo cáo từ %{since} đến %{until}
|
||||
utc_warning: Báo cáo đã được tạo với múi giờ UTC
|
||||
agent_csv:
|
||||
agent_name: Tên tổng đài viên
|
||||
conversations_count: Assigned conversations
|
||||
avg_first_response_time: Avg first response time
|
||||
conversations_count: Cuộc trò chuyện được chỉ định
|
||||
avg_first_response_time: Thời gian phản hồi đầu tiên trung bình
|
||||
avg_resolution_time: Avg resolution time
|
||||
resolution_count: Số lượng giải quyết
|
||||
avg_customer_waiting_time: Avg customer waiting time
|
||||
avg_customer_waiting_time: Thời gian chờ đợi trung bình của khách hàng
|
||||
inbox_csv:
|
||||
inbox_name: Tên kênh
|
||||
inbox_type: Kiểu kênh
|
||||
conversations_count: Số hội thoại
|
||||
avg_first_response_time: Avg first response time
|
||||
avg_resolution_time: Avg resolution time
|
||||
avg_first_response_time: Thời gian phản hồi đầu tiên trung bình
|
||||
avg_resolution_time: Thời gian giải quyết trung bình
|
||||
label_csv:
|
||||
label_title: Nhãn
|
||||
conversations_count: Số hội thoại
|
||||
avg_first_response_time: Avg first response time
|
||||
avg_resolution_time: Avg resolution time
|
||||
avg_first_response_time: Thời gian phản hồi đầu tiên trung bình
|
||||
avg_resolution_time: Thời gian giải quyết trung bình
|
||||
team_csv:
|
||||
team_name: Tên nhóm
|
||||
conversations_count: Số hội thoại
|
||||
avg_first_response_time: Avg first response time
|
||||
avg_resolution_time: Avg resolution time
|
||||
avg_first_response_time: Thời gian phản hồi đầu tiên trung bình
|
||||
avg_resolution_time: Thời gian giải quyết trung bình
|
||||
resolution_count: Số lượng giải quyết
|
||||
avg_customer_waiting_time: Avg customer waiting time
|
||||
avg_customer_waiting_time: Thời gian chờ đợi trung bình của khách hàng
|
||||
conversation_traffic_csv:
|
||||
timezone: Múi giờ
|
||||
sla_csv:
|
||||
conversation_id: Conversation ID
|
||||
conversation_id: ID hội thoại
|
||||
sla_policy_breached: SLA Policy
|
||||
assignee: Assignee
|
||||
assignee: Đại lý được chỉ định
|
||||
team: Nhóm
|
||||
inbox: Hộp thư đến
|
||||
labels: Nhãn
|
||||
@@ -118,22 +118,22 @@ vi:
|
||||
recorded_at: Ngày nghi
|
||||
notifications:
|
||||
notification_title:
|
||||
conversation_creation: "A conversation (#%{display_id}) has been created in %{inbox_name}"
|
||||
conversation_assignment: "A conversation (#%{display_id}) has been assigned to you"
|
||||
assigned_conversation_new_message: "A new message is created in conversation (#%{display_id})"
|
||||
conversation_mention: "You have been mentioned in conversation (#%{display_id})"
|
||||
sla_missed_first_response: "SLA target first response missed for conversation (#%{display_id})"
|
||||
sla_missed_next_response: "SLA target next response missed for conversation (#%{display_id})"
|
||||
sla_missed_resolution: "SLA target resolution missed for conversation (#%{display_id})"
|
||||
attachment: "Attachment"
|
||||
no_content: "No content"
|
||||
conversation_creation: "Một cuộc trò chuyện (#%{display_id}) đã được tạo trong %{inbox_name}"
|
||||
conversation_assignment: "Một cuộc trò chuyện (#%{display_id}) đã được chỉ định cho bạn"
|
||||
assigned_conversation_new_message: "Một tin nhắn mới được tạo trong cuộc trò chuyện (#%{display_id})"
|
||||
conversation_mention: "Bạn đã được nhắc đến trong cuộc trò chuyện (#%{display_id})"
|
||||
sla_missed_first_response: "Mục tiêu SLA phản hồi đầu tiên bị bỏ lỡ cho cuộc trò chuyện (#%{display_id})"
|
||||
sla_missed_next_response: "Mục tiêu SLA phản hồi tiếp theo bị bỏ lỡ cho cuộc trò chuyện (#%{display_id})"
|
||||
sla_missed_resolution: "Độ phân giải mục tiêu SLA bị bỏ lỡ cho cuộc trò chuyện (#%{display_id})"
|
||||
attachment: "Tập tin đính kèm"
|
||||
no_content: "Không có nội dung"
|
||||
conversations:
|
||||
messages:
|
||||
instagram_story_content: "%{story_sender} đã đề cập đến bạn trong hội thoại: "
|
||||
instagram_deleted_story_content: Hội thoại này không còn nữa.
|
||||
deleted: Tin nhắn đã bị xoá
|
||||
delivery_status:
|
||||
error_code: "Error code: %{error_code}"
|
||||
error_code: "Mã lỗi: %{error_code}"
|
||||
activity:
|
||||
status:
|
||||
resolved: "Cuộc trò chuyện được đánh dấu là đã giải quyết bởi %{user_name}"
|
||||
@@ -221,7 +221,7 @@ vi:
|
||||
common:
|
||||
home: Trang Chủ
|
||||
last_updated_on: 'Cập nhật lần cuối: %{last_updated_on}'
|
||||
view_all_articles: View all
|
||||
view_all_articles: Xem tất cả
|
||||
article: bài viết
|
||||
articles: bài viết
|
||||
author: tác giả
|
||||
@@ -233,17 +233,17 @@ vi:
|
||||
footer:
|
||||
made_with: Tạo bởi
|
||||
header:
|
||||
go_to_homepage: Website
|
||||
go_to_homepage: Trang web
|
||||
appearance:
|
||||
system: System
|
||||
light: Light
|
||||
dark: Dark
|
||||
featured_articles: Featured Articles
|
||||
system: Hệ thống
|
||||
light: Sáng
|
||||
dark: Tối
|
||||
featured_articles: Bài viết nổi bật
|
||||
uncategorized: Chưa được phân loại
|
||||
404:
|
||||
title: Page not found
|
||||
description: We couldn't find the page you were looking for.
|
||||
back_to_home: Go to home page
|
||||
title: Không tìm thấy trang
|
||||
description: Chúng tôi không thể tìm thấy trang bạn đang tìm kiếm.
|
||||
back_to_home: Tới trang chủ
|
||||
slack_unfurl:
|
||||
fields:
|
||||
name: Tên
|
||||
@@ -255,10 +255,10 @@ vi:
|
||||
button: Mở cuộc trò chuyện
|
||||
time_units:
|
||||
days:
|
||||
other: "%{count} days"
|
||||
other: "%{count} ngày"
|
||||
hours:
|
||||
other: "%{count} hours"
|
||||
other: "%{count} giờ"
|
||||
minutes:
|
||||
other: "%{count} minutes"
|
||||
other: "%{count} phút"
|
||||
seconds:
|
||||
other: "%{count} seconds"
|
||||
other: "%{count} giây"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@chatwoot/chatwoot",
|
||||
"version": "3.10.1",
|
||||
"version": "3.10.2",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"eslint": "eslint app/**/*.{js,vue}",
|
||||
|
||||
@@ -16,5 +16,17 @@ FactoryBot.define do
|
||||
conversation.contact ||= create(:contact, :with_email, account: conversation.account)
|
||||
conversation.contact_inbox ||= create(:contact_inbox, contact: conversation.contact, inbox: conversation.inbox)
|
||||
end
|
||||
|
||||
trait :with_team do
|
||||
after(:build) do |conversation|
|
||||
conversation.team ||= create(:team, account: conversation.account)
|
||||
end
|
||||
end
|
||||
|
||||
trait :with_assignee do
|
||||
after(:build) do |conversation|
|
||||
conversation.assignee ||= create(:user, account: conversation.account, role: :agent)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -31,18 +31,54 @@ describe ActionService do
|
||||
|
||||
describe '#assign_agent' do
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
let(:inbox_member) { create(:inbox_member, inbox: conversation.inbox, user: agent) }
|
||||
let(:conversation) { create(:conversation, :with_assignee, account: account) }
|
||||
let(:action_service) { described_class.new(conversation) }
|
||||
|
||||
it 'unassigns the conversation if agent id is nil' do
|
||||
action_service.assign_agent(['nil'])
|
||||
expect(conversation.reload.assignee).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
it 'unassigns the team if team_id is nil' do
|
||||
action_service.assign_team([nil])
|
||||
expect(conversation.reload.team).to be_nil
|
||||
describe '#assign_team' do
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:inbox_member) { create(:inbox_member, inbox: conversation.inbox, user: agent) }
|
||||
let(:team) { create(:team, name: 'ConversationTeam', account: account) }
|
||||
let(:conversation) { create(:conversation, :with_team, account: account) }
|
||||
let(:action_service) { described_class.new(conversation) }
|
||||
|
||||
context 'when team_id is not present' do
|
||||
it 'unassign the if team_id is "nil"' do
|
||||
expect do
|
||||
action_service.assign_team(['nil'])
|
||||
end.not_to raise_error
|
||||
expect(conversation.reload.team).to be_nil
|
||||
end
|
||||
|
||||
it 'unassign the if team_id is 0' do
|
||||
expect do
|
||||
action_service.assign_team([0])
|
||||
end.not_to raise_error
|
||||
expect(conversation.reload.team).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when team_id is present' do
|
||||
it 'assign the team if the team is part of the account' do
|
||||
original_team = conversation.team
|
||||
expect do
|
||||
action_service.assign_team([team.id])
|
||||
end.to change { conversation.reload.team }.from(original_team)
|
||||
end
|
||||
|
||||
it 'does not assign the team if the team is part of the account' do
|
||||
original_team = conversation.team
|
||||
invalid_team_id = 999_999_999
|
||||
expect do
|
||||
action_service.assign_team([invalid_team_id])
|
||||
end.not_to change { conversation.reload.team }.from(original_team)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user