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_rpm', require: false
|
||||||
gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
|
gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
|
||||||
gem 'scout_apm', 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-ruby', require: false
|
||||||
gem 'sentry-sidekiq', '>= 5.15.0', require: false
|
gem 'sentry-sidekiq', '>= 5.18.0', require: false
|
||||||
|
|
||||||
##-- background job processing --##
|
##-- background job processing --##
|
||||||
gem 'sidekiq', '>= 7.2.4'
|
gem 'sidekiq', '>= 7.2.4'
|
||||||
|
|||||||
18
Gemfile.lock
18
Gemfile.lock
@@ -150,7 +150,7 @@ GEM
|
|||||||
statsd-ruby (~> 1.1)
|
statsd-ruby (~> 1.1)
|
||||||
base64 (0.2.0)
|
base64 (0.2.0)
|
||||||
bcrypt (3.1.20)
|
bcrypt (3.1.20)
|
||||||
bigdecimal (3.1.7)
|
bigdecimal (3.1.8)
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
bootsnap (1.16.0)
|
bootsnap (1.16.0)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
@@ -603,7 +603,7 @@ GEM
|
|||||||
ffi (~> 1.0)
|
ffi (~> 1.0)
|
||||||
redis (5.0.6)
|
redis (5.0.6)
|
||||||
redis-client (>= 0.9.0)
|
redis-client (>= 0.9.0)
|
||||||
redis-client (0.22.1)
|
redis-client (0.22.2)
|
||||||
connection_pool
|
connection_pool
|
||||||
redis-namespace (1.10.0)
|
redis-namespace (1.10.0)
|
||||||
redis (>= 4)
|
redis (>= 4)
|
||||||
@@ -703,14 +703,14 @@ GEM
|
|||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
selectize-rails (0.12.6)
|
selectize-rails (0.12.6)
|
||||||
semantic_range (3.0.0)
|
semantic_range (3.0.0)
|
||||||
sentry-rails (5.17.3)
|
sentry-rails (5.18.0)
|
||||||
railties (>= 5.0)
|
railties (>= 5.0)
|
||||||
sentry-ruby (~> 5.17.3)
|
sentry-ruby (~> 5.18.0)
|
||||||
sentry-ruby (5.17.3)
|
sentry-ruby (5.18.0)
|
||||||
bigdecimal
|
bigdecimal
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
sentry-sidekiq (5.17.3)
|
sentry-sidekiq (5.18.0)
|
||||||
sentry-ruby (~> 5.17.3)
|
sentry-ruby (~> 5.18.0)
|
||||||
sidekiq (>= 3.0)
|
sidekiq (>= 3.0)
|
||||||
sexp_processor (4.17.0)
|
sexp_processor (4.17.0)
|
||||||
shoulda-matchers (5.3.0)
|
shoulda-matchers (5.3.0)
|
||||||
@@ -931,9 +931,9 @@ DEPENDENCIES
|
|||||||
scout_apm
|
scout_apm
|
||||||
scss_lint
|
scss_lint
|
||||||
seed_dump
|
seed_dump
|
||||||
sentry-rails (>= 5.14.0)
|
sentry-rails (>= 5.18.0)
|
||||||
sentry-ruby
|
sentry-ruby
|
||||||
sentry-sidekiq (>= 5.15.0)
|
sentry-sidekiq (>= 5.18.0)
|
||||||
shoulda-matchers
|
shoulda-matchers
|
||||||
sidekiq (>= 7.2.4)
|
sidekiq (>= 7.2.4)
|
||||||
sidekiq-cron (>= 1.12.0)
|
sidekiq-cron (>= 1.12.0)
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export const generateConditionOptions = (options, key = 'id') => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Add the "None" option to the agent list
|
// Add the "None" option to the agent list
|
||||||
export const agentList = agents => [
|
export const addNoneToList = agents => [
|
||||||
{
|
{
|
||||||
id: 'nil',
|
id: 'nil',
|
||||||
name: 'None',
|
name: 'None',
|
||||||
@@ -137,8 +137,8 @@ export const getActionOptions = ({
|
|||||||
type,
|
type,
|
||||||
}) => {
|
}) => {
|
||||||
const actionsMap = {
|
const actionsMap = {
|
||||||
assign_agent: agentList(agents),
|
assign_agent: addNoneToList(agents),
|
||||||
assign_team: teams,
|
assign_team: addNoneToList(teams),
|
||||||
send_email_to_team: teams,
|
send_email_to_team: teams,
|
||||||
add_label: generateConditionOptions(labels, 'title'),
|
add_label: generateConditionOptions(labels, 'title'),
|
||||||
remove_label: generateConditionOptions(labels, 'title'),
|
remove_label: generateConditionOptions(labels, 'title'),
|
||||||
|
|||||||
@@ -39,6 +39,14 @@
|
|||||||
:readable-time="readableTime"
|
:readable-time="readableTime"
|
||||||
@error="onImageLoadError"
|
@error="onImageLoadError"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<video-bubble
|
||||||
|
v-if="attachment.file_type === 'video' && !hasVideoError"
|
||||||
|
:url="attachment.data_url"
|
||||||
|
:readable-time="readableTime"
|
||||||
|
@error="onVideoLoadError"
|
||||||
|
/>
|
||||||
|
|
||||||
<file-bubble
|
<file-bubble
|
||||||
v-else
|
v-else
|
||||||
:url="attachment.data_url"
|
:url="attachment.data_url"
|
||||||
@@ -72,6 +80,7 @@
|
|||||||
import UserMessageBubble from 'widget/components/UserMessageBubble.vue';
|
import UserMessageBubble from 'widget/components/UserMessageBubble.vue';
|
||||||
import MessageReplyButton from 'widget/components/MessageReplyButton.vue';
|
import MessageReplyButton from 'widget/components/MessageReplyButton.vue';
|
||||||
import ImageBubble from 'widget/components/ImageBubble.vue';
|
import ImageBubble from 'widget/components/ImageBubble.vue';
|
||||||
|
import VideoBubble from 'widget/components/VideoBubble.vue';
|
||||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||||
import FileBubble from 'widget/components/FileBubble.vue';
|
import FileBubble from 'widget/components/FileBubble.vue';
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
import timeMixin from 'dashboard/mixins/time';
|
||||||
@@ -88,6 +97,7 @@ export default {
|
|||||||
UserMessageBubble,
|
UserMessageBubble,
|
||||||
MessageReplyButton,
|
MessageReplyButton,
|
||||||
ImageBubble,
|
ImageBubble,
|
||||||
|
VideoBubble,
|
||||||
FileBubble,
|
FileBubble,
|
||||||
FluentIcon,
|
FluentIcon,
|
||||||
ReplyToChip,
|
ReplyToChip,
|
||||||
@@ -107,6 +117,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
hasImageError: false,
|
hasImageError: false,
|
||||||
|
hasVideoError: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -143,10 +154,12 @@ export default {
|
|||||||
watch: {
|
watch: {
|
||||||
message() {
|
message() {
|
||||||
this.hasImageError = false;
|
this.hasImageError = false;
|
||||||
|
this.hasVideoError = false;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.hasImageError = false;
|
this.hasImageError = false;
|
||||||
|
this.hasVideoError = false;
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async retrySendMessage() {
|
async retrySendMessage() {
|
||||||
@@ -158,6 +171,9 @@ export default {
|
|||||||
onImageLoadError() {
|
onImageLoadError() {
|
||||||
this.hasImageError = true;
|
this.hasImageError = true;
|
||||||
},
|
},
|
||||||
|
onVideoLoadError() {
|
||||||
|
this.hasVideoError = true;
|
||||||
|
},
|
||||||
toggleReply() {
|
toggleReply() {
|
||||||
this.$emitter.emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.message);
|
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
|
# Add validation errors to the record with a formatted statement
|
||||||
validation_errors.each do |error|
|
validation_errors.each do |error|
|
||||||
# byebug
|
|
||||||
format_and_append_error(error, record)
|
format_and_append_error(error, record)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -50,7 +50,11 @@ class ActionService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def assign_team(team_ids = [])
|
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
|
# 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
|
# 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)
|
return unless !team_ids[0].nil? && team_belongs_to_account?(team_ids)
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class Notification::PushNotificationService
|
|||||||
def send_browser_push(subscription)
|
def send_browser_push(subscription)
|
||||||
return unless can_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]}")
|
Rails.logger.info("Browser push sent to #{user.email} with title #{push_message[:title]}")
|
||||||
rescue WebPush::ExpiredSubscription, WebPush::InvalidSubscription, WebPush::Unauthorized => e
|
rescue WebPush::ExpiredSubscription, WebPush::InvalidSubscription, WebPush::Unauthorized => e
|
||||||
Rails.logger.info "WebPush subscription expired: #{e.message}"
|
Rails.logger.info "WebPush subscription expired: #{e.message}"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
shared: &shared
|
shared: &shared
|
||||||
version: '3.10.1'
|
version: '3.10.2'
|
||||||
|
|
||||||
development:
|
development:
|
||||||
<<: *shared
|
<<: *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
|
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ệ
|
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}"
|
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
|
failed: Đăng ký thât bại
|
||||||
data_import:
|
data_import:
|
||||||
data_type:
|
data_type:
|
||||||
@@ -51,7 +51,7 @@ vi:
|
|||||||
dyte:
|
dyte:
|
||||||
invalid_message_type: "Loại tin nhắn không hợp lệ. Hành động không được phép"
|
invalid_message_type: "Loại tin nhắn không hợp lệ. Hành động không được phép"
|
||||||
slack:
|
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:
|
inboxes:
|
||||||
imap:
|
imap:
|
||||||
socket_error: Vui lòng kiểm tra kết nối mạng, địa chỉ IMAP và thử lại.
|
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ự < > / \ @.
|
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:
|
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.
|
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_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: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}].
|
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: Invalid value. The values provided for %{attribute_name} are invalid
|
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:
|
reports:
|
||||||
period: Thời gian báo cáo từ %{since} đến %{until}
|
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
|
utc_warning: Báo cáo đã được tạo với múi giờ UTC
|
||||||
agent_csv:
|
agent_csv:
|
||||||
agent_name: Tên tổng đài viên
|
agent_name: Tên tổng đài viên
|
||||||
conversations_count: Assigned conversations
|
conversations_count: Cuộc trò chuyện được chỉ định
|
||||||
avg_first_response_time: Avg first response time
|
avg_first_response_time: Thời gian phản hồi đầu tiên trung bình
|
||||||
avg_resolution_time: Avg resolution time
|
avg_resolution_time: Avg resolution time
|
||||||
resolution_count: Số lượng giải quyết
|
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_csv:
|
||||||
inbox_name: Tên kênh
|
inbox_name: Tên kênh
|
||||||
inbox_type: Kiểu kênh
|
inbox_type: Kiểu kênh
|
||||||
conversations_count: Số hội thoại
|
conversations_count: Số hội thoại
|
||||||
avg_first_response_time: Avg first response time
|
avg_first_response_time: Thời gian phản hồi đầu tiên trung bình
|
||||||
avg_resolution_time: Avg resolution time
|
avg_resolution_time: Thời gian giải quyết trung bình
|
||||||
label_csv:
|
label_csv:
|
||||||
label_title: Nhãn
|
label_title: Nhãn
|
||||||
conversations_count: Số hội thoại
|
conversations_count: Số hội thoại
|
||||||
avg_first_response_time: Avg first response time
|
avg_first_response_time: Thời gian phản hồi đầu tiên trung bình
|
||||||
avg_resolution_time: Avg resolution time
|
avg_resolution_time: Thời gian giải quyết trung bình
|
||||||
team_csv:
|
team_csv:
|
||||||
team_name: Tên nhóm
|
team_name: Tên nhóm
|
||||||
conversations_count: Số hội thoại
|
conversations_count: Số hội thoại
|
||||||
avg_first_response_time: Avg first response time
|
avg_first_response_time: Thời gian phản hồi đầu tiên trung bình
|
||||||
avg_resolution_time: Avg resolution time
|
avg_resolution_time: Thời gian giải quyết trung bình
|
||||||
resolution_count: Số lượng giải quyết
|
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:
|
conversation_traffic_csv:
|
||||||
timezone: Múi giờ
|
timezone: Múi giờ
|
||||||
sla_csv:
|
sla_csv:
|
||||||
conversation_id: Conversation ID
|
conversation_id: ID hội thoại
|
||||||
sla_policy_breached: SLA Policy
|
sla_policy_breached: SLA Policy
|
||||||
assignee: Assignee
|
assignee: Đại lý được chỉ định
|
||||||
team: Nhóm
|
team: Nhóm
|
||||||
inbox: Hộp thư đến
|
inbox: Hộp thư đến
|
||||||
labels: Nhãn
|
labels: Nhãn
|
||||||
@@ -118,22 +118,22 @@ vi:
|
|||||||
recorded_at: Ngày nghi
|
recorded_at: Ngày nghi
|
||||||
notifications:
|
notifications:
|
||||||
notification_title:
|
notification_title:
|
||||||
conversation_creation: "A conversation (#%{display_id}) has been created in %{inbox_name}"
|
conversation_creation: "Một cuộc trò chuyện (#%{display_id}) đã được tạo trong %{inbox_name}"
|
||||||
conversation_assignment: "A conversation (#%{display_id}) has been assigned to you"
|
conversation_assignment: "Một cuộc trò chuyện (#%{display_id}) đã được chỉ định cho bạn"
|
||||||
assigned_conversation_new_message: "A new message is created in conversation (#%{display_id})"
|
assigned_conversation_new_message: "Một tin nhắn mới được tạo trong cuộc trò chuyện (#%{display_id})"
|
||||||
conversation_mention: "You have been mentioned in conversation (#%{display_id})"
|
conversation_mention: "Bạn đã được nhắc đến trong cuộc trò chuyện (#%{display_id})"
|
||||||
sla_missed_first_response: "SLA target first response missed for conversation (#%{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: "SLA target next response missed for conversation (#%{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: "SLA target resolution missed for conversation (#%{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: "Attachment"
|
attachment: "Tập tin đính kèm"
|
||||||
no_content: "No content"
|
no_content: "Không có nội dung"
|
||||||
conversations:
|
conversations:
|
||||||
messages:
|
messages:
|
||||||
instagram_story_content: "%{story_sender} đã đề cập đến bạn trong hội thoại: "
|
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.
|
instagram_deleted_story_content: Hội thoại này không còn nữa.
|
||||||
deleted: Tin nhắn đã bị xoá
|
deleted: Tin nhắn đã bị xoá
|
||||||
delivery_status:
|
delivery_status:
|
||||||
error_code: "Error code: %{error_code}"
|
error_code: "Mã lỗi: %{error_code}"
|
||||||
activity:
|
activity:
|
||||||
status:
|
status:
|
||||||
resolved: "Cuộc trò chuyện được đánh dấu là đã giải quyết bởi %{user_name}"
|
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:
|
common:
|
||||||
home: Trang Chủ
|
home: Trang Chủ
|
||||||
last_updated_on: 'Cập nhật lần cuối: %{last_updated_on}'
|
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
|
article: bài viết
|
||||||
articles: bài viết
|
articles: bài viết
|
||||||
author: tác giả
|
author: tác giả
|
||||||
@@ -233,17 +233,17 @@ vi:
|
|||||||
footer:
|
footer:
|
||||||
made_with: Tạo bởi
|
made_with: Tạo bởi
|
||||||
header:
|
header:
|
||||||
go_to_homepage: Website
|
go_to_homepage: Trang web
|
||||||
appearance:
|
appearance:
|
||||||
system: System
|
system: Hệ thống
|
||||||
light: Light
|
light: Sáng
|
||||||
dark: Dark
|
dark: Tối
|
||||||
featured_articles: Featured Articles
|
featured_articles: Bài viết nổi bật
|
||||||
uncategorized: Chưa được phân loại
|
uncategorized: Chưa được phân loại
|
||||||
404:
|
404:
|
||||||
title: Page not found
|
title: Không tìm thấy trang
|
||||||
description: We couldn't find the page you were looking for.
|
description: Chúng tôi không thể tìm thấy trang bạn đang tìm kiếm.
|
||||||
back_to_home: Go to home page
|
back_to_home: Tới trang chủ
|
||||||
slack_unfurl:
|
slack_unfurl:
|
||||||
fields:
|
fields:
|
||||||
name: Tên
|
name: Tên
|
||||||
@@ -255,10 +255,10 @@ vi:
|
|||||||
button: Mở cuộc trò chuyện
|
button: Mở cuộc trò chuyện
|
||||||
time_units:
|
time_units:
|
||||||
days:
|
days:
|
||||||
other: "%{count} days"
|
other: "%{count} ngày"
|
||||||
hours:
|
hours:
|
||||||
other: "%{count} hours"
|
other: "%{count} giờ"
|
||||||
minutes:
|
minutes:
|
||||||
other: "%{count} minutes"
|
other: "%{count} phút"
|
||||||
seconds:
|
seconds:
|
||||||
other: "%{count} seconds"
|
other: "%{count} giây"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@chatwoot/chatwoot",
|
"name": "@chatwoot/chatwoot",
|
||||||
"version": "3.10.1",
|
"version": "3.10.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"eslint": "eslint app/**/*.{js,vue}",
|
"eslint": "eslint app/**/*.{js,vue}",
|
||||||
|
|||||||
@@ -16,5 +16,17 @@ FactoryBot.define do
|
|||||||
conversation.contact ||= create(:contact, :with_email, account: conversation.account)
|
conversation.contact ||= create(:contact, :with_email, account: conversation.account)
|
||||||
conversation.contact_inbox ||= create(:contact_inbox, contact: conversation.contact, inbox: conversation.inbox)
|
conversation.contact_inbox ||= create(:contact_inbox, contact: conversation.contact, inbox: conversation.inbox)
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -31,18 +31,54 @@ describe ActionService do
|
|||||||
|
|
||||||
describe '#assign_agent' do
|
describe '#assign_agent' do
|
||||||
let(:agent) { create(:user, account: account, role: :agent) }
|
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(: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) }
|
let(:action_service) { described_class.new(conversation) }
|
||||||
|
|
||||||
it 'unassigns the conversation if agent id is nil' do
|
it 'unassigns the conversation if agent id is nil' do
|
||||||
action_service.assign_agent(['nil'])
|
action_service.assign_agent(['nil'])
|
||||||
expect(conversation.reload.assignee).to be_nil
|
expect(conversation.reload.assignee).to be_nil
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it 'unassigns the team if team_id is nil' do
|
describe '#assign_team' do
|
||||||
action_service.assign_team([nil])
|
let(:agent) { create(:user, account: account, role: :agent) }
|
||||||
expect(conversation.reload.team).to be_nil
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user