diff --git a/Gemfile b/Gemfile index 43f255074..5e1e5917f 100644 --- a/Gemfile +++ b/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' diff --git a/Gemfile.lock b/Gemfile.lock index bbb1aa063..04a8fb534 100644 --- a/Gemfile.lock +++ b/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) diff --git a/app/javascript/dashboard/helper/automationHelper.js b/app/javascript/dashboard/helper/automationHelper.js index 665e8faf6..33e920edc 100644 --- a/app/javascript/dashboard/helper/automationHelper.js +++ b/app/javascript/dashboard/helper/automationHelper.js @@ -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'), diff --git a/app/javascript/widget/components/UserMessage.vue b/app/javascript/widget/components/UserMessage.vue index 0631b2f90..5a97422db 100755 --- a/app/javascript/widget/components/UserMessage.vue +++ b/app/javascript/widget/components/UserMessage.vue @@ -39,6 +39,14 @@ :readable-time="readableTime" @error="onImageLoadError" /> + + + +defineProps({ + url: { type: String, default: '' }, + readableTime: { type: String, default: '' }, +}); + +const emits = defineEmits(['error']); + +const onVideoError = () => { + emits('error'); +}; + + diff --git a/app/models/concerns/json_schema_validator.rb b/app/models/concerns/json_schema_validator.rb index 4ae94df12..808564be9 100644 --- a/app/models/concerns/json_schema_validator.rb +++ b/app/models/concerns/json_schema_validator.rb @@ -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 diff --git a/app/services/action_service.rb b/app/services/action_service.rb index 79f1234a0..4f33a302b 100644 --- a/app/services/action_service.rb +++ b/app/services/action_service.rb @@ -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) diff --git a/app/services/notification/push_notification_service.rb b/app/services/notification/push_notification_service.rb index 263800d4a..9878107c1 100644 --- a/app/services/notification/push_notification_service.rb +++ b/app/services/notification/push_notification_service.rb @@ -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}" diff --git a/config/app.yml b/config/app.yml index 8a133ee53..b8c238254 100644 --- a/config/app.yml +++ b/config/app.yml @@ -1,5 +1,5 @@ shared: &shared - version: '3.10.1' + version: '3.10.2' development: <<: *shared diff --git a/config/locales/vi.yml b/config/locales/vi.yml index 189184d4d..8fb3e4507 100644 --- a/config/locales/vi.yml +++ b/config/locales/vi.yml @@ -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" diff --git a/package.json b/package.json index 6b017503e..79ac41e02 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@chatwoot/chatwoot", - "version": "3.10.1", + "version": "3.10.2", "license": "MIT", "scripts": { "eslint": "eslint app/**/*.{js,vue}", diff --git a/spec/factories/conversations.rb b/spec/factories/conversations.rb index 6f2bc34ef..c552e6c30 100644 --- a/spec/factories/conversations.rb +++ b/spec/factories/conversations.rb @@ -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 diff --git a/spec/services/action_service_spec.rb b/spec/services/action_service_spec.rb index bdc9c88ff..c28761844 100644 --- a/spec/services/action_service_spec.rb +++ b/spec/services/action_service_spec.rb @@ -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