From 87f758ee1f7ce39869a40902b315c08a2133686b Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Thu, 11 May 2023 12:56:43 +0530 Subject: [PATCH 01/31] feat: Order conversations by priority (#7053) --- app/finders/conversation_finder.rb | 3 +- app/javascript/dashboard/constants/globals.js | 1 + .../dashboard/i18n/locale/en/chatlist.json | 3 + .../dashboard/mixins/specs/time.spec.js | 1 + .../store/modules/conversations/getters.js | 11 +++- .../specs/conversations/getters.spec.js | 60 +++++++++++++++++++ app/javascript/shared/constants/messages.js | 8 +++ 7 files changed, 85 insertions(+), 2 deletions(-) diff --git a/app/finders/conversation_finder.rb b/app/finders/conversation_finder.rb index 9472dc623..074eed461 100644 --- a/app/finders/conversation_finder.rb +++ b/app/finders/conversation_finder.rb @@ -5,7 +5,8 @@ class ConversationFinder SORT_OPTIONS = { latest: 'latest', sort_on_created_at: 'sort_on_created_at', - last_user_message_at: 'last_user_message_at' + last_user_message_at: 'last_user_message_at', + sort_on_priority: 'sort_on_priority' }.with_indifferent_access # assumptions diff --git a/app/javascript/dashboard/constants/globals.js b/app/javascript/dashboard/constants/globals.js index 20497b039..e5aaf7814 100644 --- a/app/javascript/dashboard/constants/globals.js +++ b/app/javascript/dashboard/constants/globals.js @@ -15,6 +15,7 @@ export default { SORT_BY_TYPE: { LATEST: 'latest', CREATED_AT: 'sort_on_created_at', + PRIORITY: 'sort_on_priority', }, ARTICLE_STATUS_TYPES: { DRAFT: 0, diff --git a/app/javascript/dashboard/i18n/locale/en/chatlist.json b/app/javascript/dashboard/i18n/locale/en/chatlist.json index 6312d4c9f..731748696 100644 --- a/app/javascript/dashboard/i18n/locale/en/chatlist.json +++ b/app/javascript/dashboard/i18n/locale/en/chatlist.json @@ -47,6 +47,9 @@ }, "sort_on_created_at": { "TEXT": "Created at" + }, + "sort_on_priority": { + "TEXT": "Priority" } }, "ATTACHMENTS": { diff --git a/app/javascript/dashboard/mixins/specs/time.spec.js b/app/javascript/dashboard/mixins/specs/time.spec.js index 56b70ec8a..b1cd46d31 100644 --- a/app/javascript/dashboard/mixins/specs/time.spec.js +++ b/app/javascript/dashboard/mixins/specs/time.spec.js @@ -24,6 +24,7 @@ describe('#messageTimestamp', () => { describe('#dynamicTime', () => { it('returns correct value', () => { + Date.now = jest.fn(() => new Date(Date.UTC(2023, 1, 14)).valueOf()); expect(TimeMixin.methods.dynamicTime(1612971343)).toEqual( 'about 2 years ago' ); diff --git a/app/javascript/dashboard/store/modules/conversations/getters.js b/app/javascript/dashboard/store/modules/conversations/getters.js index d3a848d09..b5b38da0f 100644 --- a/app/javascript/dashboard/store/modules/conversations/getters.js +++ b/app/javascript/dashboard/store/modules/conversations/getters.js @@ -1,4 +1,7 @@ -import { MESSAGE_TYPE } from 'shared/constants/messages'; +import { + MESSAGE_TYPE, + CONVERSATION_PRIORITY_ORDER, +} from 'shared/constants/messages'; import { applyPageFilters } from './helpers'; export const getSelectedChatConversation = ({ @@ -13,6 +16,12 @@ const getters = { const comparator = { latest: (a, b) => b.last_activity_at - a.last_activity_at, sort_on_created_at: (a, b) => a.created_at - b.created_at, + sort_on_priority: (a, b) => { + return ( + CONVERSATION_PRIORITY_ORDER[a.priority] - + CONVERSATION_PRIORITY_ORDER[b.priority] + ); + }, }; return allConversations.sort(comparator[chatSortFilter]); diff --git a/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js b/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js index f9d775df2..2a012fd53 100644 --- a/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js +++ b/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js @@ -130,6 +130,66 @@ describe('#getters', () => { }, ]); }); + it('order conversations based on priority', () => { + const state = { + allConversations: [ + { + id: 1, + messages: [ + { + content: 'test1', + }, + ], + priority: 'low', + created_at: 1683645801, + last_activity_at: 2466424490, + }, + { + id: 2, + messages: [{ content: 'test2' }], + priority: 'urgent', + created_at: 1652109801, + last_activity_at: 1466424480, + }, + { + id: 3, + messages: [{ content: 'test3' }], + priority: 'medium', + created_at: 1652109801, + last_activity_at: 1466421280, + }, + ], + chatSortFilter: 'sort_on_priority', + }; + + expect(getters.getAllConversations(state)).toEqual([ + { + id: 2, + messages: [{ content: 'test2' }], + priority: 'urgent', + created_at: 1652109801, + last_activity_at: 1466424480, + }, + { + id: 3, + messages: [{ content: 'test3' }], + priority: 'medium', + created_at: 1652109801, + last_activity_at: 1466421280, + }, + { + id: 1, + messages: [ + { + content: 'test1', + }, + ], + priority: 'low', + created_at: 1683645801, + last_activity_at: 2466424490, + }, + ]); + }); }); describe('#getUnAssignedChats', () => { it('order returns only chats assigned to user', () => { diff --git a/app/javascript/shared/constants/messages.js b/app/javascript/shared/constants/messages.js index 77501cb76..c21248607 100644 --- a/app/javascript/shared/constants/messages.js +++ b/app/javascript/shared/constants/messages.js @@ -27,6 +27,14 @@ export const CONVERSATION_PRIORITY = { MEDIUM: 'medium', }; +export const CONVERSATION_PRIORITY_ORDER = { + urgent: 1, + high: 2, + medium: 3, + low: 4, + null: 5, +}; + // Size in mega bytes export const MAXIMUM_FILE_UPLOAD_SIZE = 40; export const MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL = 5; From 9c5d062efc7d769635dd079064df664cb952f68d Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 11 May 2023 17:30:07 +0530 Subject: [PATCH 02/31] fix: Fix greeting message label in settings (#7056) --- app/javascript/dashboard/i18n/locale/en/inboxMgmt.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index afe22286f..06af3794e 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -83,7 +83,7 @@ }, "CHANNEL_GREETING_TOGGLE": { "LABEL": "Enable channel greeting", - "HELP_TEXT": "Automatically send a greeting message when a new conversation is created.", + "HELP_TEXT": "Automatically send a greeting message after the contact's first message in a conversation.", "ENABLED": "Enabled", "DISABLED": "Disabled" }, From d99997d17de90739e8b0ab4b559d195c99480f53 Mon Sep 17 00:00:00 2001 From: Jamie Wood Date: Thu, 11 May 2023 13:02:29 +0100 Subject: [PATCH 03/31] feat: Add ability to filter Conversations by underlying `source_id` (#6979) This change adds the ability to include a `source_id` param when querying the `/api/v1/accounts/{account_id}/conversations/search` endpoint. It restricts to results to only conversations related to a contact_inbox with the provided parameter. My motivation for adding this feature was to allow an external API to communicate with a specific conversation with only an awareness of the conversation `source_id` from the client. Co-authored-by: Sojan Jose --- app/finders/conversation_finder.rb | 20 +++++++++++++++++--- spec/finders/conversation_finder_spec.rb | 21 ++++++++++++++++++++- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/app/finders/conversation_finder.rb b/app/finders/conversation_finder.rb index 074eed461..7ec0a6d30 100644 --- a/app/finders/conversation_finder.rb +++ b/app/finders/conversation_finder.rb @@ -54,9 +54,10 @@ class ConversationFinder find_all_conversations filter_by_status unless params[:q] - filter_by_team if @team - filter_by_labels if params[:labels] - filter_by_query if params[:q] + filter_by_team + filter_by_labels + filter_by_query + filter_by_source_id end def set_inboxes @@ -107,6 +108,8 @@ class ConversationFinder end def filter_by_query + return unless params[:q] + allowed_message_types = [Message.message_types[:incoming], Message.message_types[:outgoing]] @conversations = conversations.joins(:messages).where('messages.content ILIKE :search', search: "%#{params[:q]}%") .where(messages: { message_type: allowed_message_types }).includes(:messages) @@ -121,13 +124,24 @@ class ConversationFinder end def filter_by_team + return unless @team + @conversations = @conversations.where(team: @team) end def filter_by_labels + return unless params[:labels] + @conversations = @conversations.tagged_with(params[:labels], any: true) end + def filter_by_source_id + return unless params[:source_id] + + @conversations = @conversations.joins(:contact_inbox) + @conversations = @conversations.where(contact_inboxes: { source_id: params[:source_id] }) + end + def set_count_for_all_conversations [ @conversations.assigned_to(current_user).count, diff --git a/spec/finders/conversation_finder_spec.rb b/spec/finders/conversation_finder_spec.rb index 15e4ff127..91a257e13 100644 --- a/spec/finders/conversation_finder_spec.rb +++ b/spec/finders/conversation_finder_spec.rb @@ -8,6 +8,7 @@ describe ::ConversationFinder do let!(:user_2) { create(:user, account: account) } let!(:admin) { create(:user, account: account, role: :administrator) } let!(:inbox) { create(:inbox, account: account, enable_auto_assignment: false) } + let!(:contact_inbox) { create(:contact_inbox, inbox: inbox, source_id: 'testing_source_id') } let!(:restricted_inbox) { create(:inbox, account: account) } before do @@ -16,7 +17,7 @@ describe ::ConversationFinder do create(:conversation, account: account, inbox: inbox, assignee: user_1) create(:conversation, account: account, inbox: inbox, assignee: user_1) create(:conversation, account: account, inbox: inbox, assignee: user_1, status: 'resolved') - create(:conversation, account: account, inbox: inbox, assignee: user_2) + create(:conversation, account: account, inbox: inbox, assignee: user_2, contact_inbox: contact_inbox) # unassigned conversation create(:conversation, account: account, inbox: inbox) Current.account = account @@ -127,6 +128,24 @@ describe ::ConversationFinder do end end + context 'with source_id' do + let(:params) { { source_id: 'testing_source_id' } } + + it 'filter conversations by source id' do + result = conversation_finder.perform + expect(result[:conversations].length).to be 1 + end + end + + context 'without source' do + let(:params) { {} } + + it 'returns conversations with any source' do + result = conversation_finder.perform + expect(result[:conversations].length).to be 4 + end + end + context 'with pagination' do let(:params) { { status: 'open', assignee_type: 'me', page: 1 } } From c97d6021e009a4639f3cc697cb4f514d3f8ae3cc Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Thu, 11 May 2023 17:35:19 +0530 Subject: [PATCH 04/31] chore: Add migration to set the default empty string value for contact name (#7052) Fixes: https://linear.app/chatwoot/issue/CW-1650/issue-with-the-contact-name --- app/models/contact.rb | 2 +- ...230510113208_set_default_empty_string_for_contact_name.rb | 5 +++++ db/schema.rb | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20230510113208_set_default_empty_string_for_contact_name.rb diff --git a/app/models/contact.rb b/app/models/contact.rb index 4f7832ac8..934fbd387 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -8,7 +8,7 @@ # email :string # identifier :string # last_activity_at :datetime -# name :string +# name :string default("") # phone_number :string # created_at :datetime not null # updated_at :datetime not null diff --git a/db/migrate/20230510113208_set_default_empty_string_for_contact_name.rb b/db/migrate/20230510113208_set_default_empty_string_for_contact_name.rb new file mode 100644 index 000000000..ae28c4318 --- /dev/null +++ b/db/migrate/20230510113208_set_default_empty_string_for_contact_name.rb @@ -0,0 +1,5 @@ +class SetDefaultEmptyStringForContactName < ActiveRecord::Migration[7.0] + def change + change_column_default :contacts, :name, from: nil, to: '' + end +end diff --git a/db/schema.rb b/db/schema.rb index d76da249a..459e97320 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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_05_10_060828) do +ActiveRecord::Schema[7.0].define(version: 2023_05_10_113208) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" @@ -394,7 +394,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_05_10_060828) do end create_table "contacts", id: :serial, force: :cascade do |t| - t.string "name" + t.string "name", default: "" t.string "email" t.string "phone_number" t.integer "account_id", null: false From 271263bcc21c80c2441e2728387515c7ab88521e Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Thu, 11 May 2023 22:42:56 +0530 Subject: [PATCH 05/31] feat: SLA CRUD APIs (EE) (#7027) Fixes: https://linear.app/chatwoot/issue/CW-1613/sla-api Co-authored-by: Sojan Jose --- app/models/account.rb | 1 + config/routes.rb | 1 + .../20230503101201_create_sla_policies.rb | 13 ++ db/schema.rb | 11 + .../api/v1/accounts/audit_logs_controller.rb | 9 +- .../enterprise_accounts_controller.rb | 8 + .../v1/accounts/sla_policies_controller.rb | 31 +++ .../enterprise/enterprise_account_concern.rb | 7 + enterprise/app/models/sla_policy.rb | 21 ++ enterprise/app/policies/sla_policy_policy.rb | 21 ++ .../sla_policies/create.json.jbuilder | 3 + .../accounts/sla_policies/index.json.jbuilder | 5 + .../accounts/sla_policies/show.json.jbuilder | 3 + .../sla_policies/update.json.jbuilder | 3 + .../api/v1/models/_sla_policy.json.jbuilder | 5 + .../accounts/sla_policies_controller_spec.rb | 189 ++++++++++++++++++ spec/enterprise/models/account_spec.rb | 18 ++ spec/enterprise/models/sla_policy_spec.rb | 22 ++ spec/factories/sla_policies.rb | 9 + 19 files changed, 372 insertions(+), 8 deletions(-) create mode 100644 db/migrate/20230503101201_create_sla_policies.rb create mode 100644 enterprise/app/controllers/api/v1/accounts/enterprise_accounts_controller.rb create mode 100644 enterprise/app/controllers/api/v1/accounts/sla_policies_controller.rb create mode 100644 enterprise/app/models/enterprise/enterprise_account_concern.rb create mode 100644 enterprise/app/models/sla_policy.rb create mode 100644 enterprise/app/policies/sla_policy_policy.rb create mode 100644 enterprise/app/views/api/v1/accounts/sla_policies/create.json.jbuilder create mode 100644 enterprise/app/views/api/v1/accounts/sla_policies/index.json.jbuilder create mode 100644 enterprise/app/views/api/v1/accounts/sla_policies/show.json.jbuilder create mode 100644 enterprise/app/views/api/v1/accounts/sla_policies/update.json.jbuilder create mode 100644 enterprise/app/views/api/v1/models/_sla_policy.json.jbuilder create mode 100644 spec/enterprise/controllers/api/v1/accounts/sla_policies_controller_spec.rb create mode 100644 spec/enterprise/models/sla_policy_spec.rb create mode 100644 spec/factories/sla_policies.rb diff --git a/app/models/account.rb b/app/models/account.rb index b415680ee..331343de1 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -149,4 +149,5 @@ class Account < ApplicationRecord end Account.prepend_mod_with('Account') +Account.include_mod_with('EnterpriseAccountConcern') Account.include_mod_with('Audit::Account') diff --git a/config/routes.rb b/config/routes.rb index 3cb03a1de..bdad7c1bf 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -64,6 +64,7 @@ Rails.application.routes.draw do post :execute, on: :member post :attach_file, on: :collection end + resources :sla_policies, only: [:index, :create, :show, :update, :destroy] resources :campaigns, only: [:index, :create, :show, :update, :destroy] resources :dashboard_apps, only: [:index, :show, :create, :update, :destroy] namespace :channels do diff --git a/db/migrate/20230503101201_create_sla_policies.rb b/db/migrate/20230503101201_create_sla_policies.rb new file mode 100644 index 000000000..9c0abce25 --- /dev/null +++ b/db/migrate/20230503101201_create_sla_policies.rb @@ -0,0 +1,13 @@ +class CreateSlaPolicies < ActiveRecord::Migration[6.1] + def change + create_table :sla_policies do |t| + t.string :name, null: false + t.float :frt_threshold, default: nil + t.float :rt_threshold, default: nil + t.boolean 'only_during_business_hours', default: false + t.references :account, index: true, null: false + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 459e97320..befec82f7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -804,6 +804,17 @@ ActiveRecord::Schema[7.0].define(version: 2023_05_10_113208) do t.index ["user_id"], name: "index_reporting_events_on_user_id" end + create_table "sla_policies", force: :cascade do |t| + t.string "name", null: false + t.float "frt_threshold" + t.float "rt_threshold" + t.boolean "only_during_business_hours", default: false + t.bigint "account_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id"], name: "index_sla_policies_on_account_id" + end + create_table "taggings", id: :serial, force: :cascade do |t| t.integer "tag_id" t.string "taggable_type" diff --git a/enterprise/app/controllers/api/v1/accounts/audit_logs_controller.rb b/enterprise/app/controllers/api/v1/accounts/audit_logs_controller.rb index d1e6b9287..b2dca5ce6 100644 --- a/enterprise/app/controllers/api/v1/accounts/audit_logs_controller.rb +++ b/enterprise/app/controllers/api/v1/accounts/audit_logs_controller.rb @@ -1,16 +1,9 @@ -# module Enterprise::Api::V1::Accounts::AuditLogsController < Api::V1::Accounts::BaseController -class Api::V1::Accounts::AuditLogsController < Api::V1::Accounts::BaseController +class Api::V1::Accounts::AuditLogsController < Api::V1::Accounts::EnterpriseAccountsController before_action :check_admin_authorization? before_action :fetch_audit - before_action :prepend_view_paths RESULTS_PER_PAGE = 15 - # Prepend the view path to the enterprise/app/views won't be available by default - def prepend_view_paths - prepend_view_path 'enterprise/app/views/' - end - def show @audit_logs = @audit_logs.page(params[:page]).per(RESULTS_PER_PAGE) @current_page = @audit_logs.current_page diff --git a/enterprise/app/controllers/api/v1/accounts/enterprise_accounts_controller.rb b/enterprise/app/controllers/api/v1/accounts/enterprise_accounts_controller.rb new file mode 100644 index 000000000..f3028721c --- /dev/null +++ b/enterprise/app/controllers/api/v1/accounts/enterprise_accounts_controller.rb @@ -0,0 +1,8 @@ +class Api::V1::Accounts::EnterpriseAccountsController < Api::V1::Accounts::BaseController + before_action :prepend_view_paths + + # Prepend the view path to the enterprise/app/views won't be available by default + def prepend_view_paths + prepend_view_path 'enterprise/app/views/' + end +end diff --git a/enterprise/app/controllers/api/v1/accounts/sla_policies_controller.rb b/enterprise/app/controllers/api/v1/accounts/sla_policies_controller.rb new file mode 100644 index 000000000..fa79c5362 --- /dev/null +++ b/enterprise/app/controllers/api/v1/accounts/sla_policies_controller.rb @@ -0,0 +1,31 @@ +class Api::V1::Accounts::SlaPoliciesController < Api::V1::Accounts::EnterpriseAccountsController + before_action :fetch_sla, only: [:show, :update, :destroy] + before_action :check_authorization + + def index + @sla_policies = Current.account.sla_policies + end + + def create + @sla_policy = Current.account.sla_policies.create!(permitted_params) + end + + def show; end + + def update + @sla_policy.update!(permitted_params) + end + + def destroy + @sla_policy.destroy! + head :ok + end + + def permitted_params + params.require(:sla_policy).permit(:name, :rt_threshold, :frt_threshold, :only_during_business_hours) + end + + def fetch_sla + @sla_policy = Current.account.sla_policies.find_by(id: params[:id]) + end +end diff --git a/enterprise/app/models/enterprise/enterprise_account_concern.rb b/enterprise/app/models/enterprise/enterprise_account_concern.rb new file mode 100644 index 000000000..8e430dbe7 --- /dev/null +++ b/enterprise/app/models/enterprise/enterprise_account_concern.rb @@ -0,0 +1,7 @@ +module Enterprise::EnterpriseAccountConcern + extend ActiveSupport::Concern + + included do + has_many :sla_policies, dependent: :destroy_async + end +end diff --git a/enterprise/app/models/sla_policy.rb b/enterprise/app/models/sla_policy.rb new file mode 100644 index 000000000..042cc2444 --- /dev/null +++ b/enterprise/app/models/sla_policy.rb @@ -0,0 +1,21 @@ +# == Schema Information +# +# Table name: sla_policies +# +# id :bigint not null, primary key +# frt_threshold :float +# name :string not null +# only_during_business_hours :boolean default(FALSE) +# rt_threshold :float +# created_at :datetime not null +# updated_at :datetime not null +# account_id :bigint not null +# +# Indexes +# +# index_sla_policies_on_account_id (account_id) +# +class SlaPolicy < ApplicationRecord + belongs_to :account + validates :name, presence: true +end diff --git a/enterprise/app/policies/sla_policy_policy.rb b/enterprise/app/policies/sla_policy_policy.rb new file mode 100644 index 000000000..4a2ca6503 --- /dev/null +++ b/enterprise/app/policies/sla_policy_policy.rb @@ -0,0 +1,21 @@ +class SlaPolicyPolicy < ApplicationPolicy + def index? + @account_user.administrator? || @account_user.agent? + end + + def update? + @account_user.administrator? + end + + def show? + @account_user.administrator? || @account_user.agent? + end + + def create? + @account_user.administrator? + end + + def destroy? + @account_user.administrator? + end +end diff --git a/enterprise/app/views/api/v1/accounts/sla_policies/create.json.jbuilder b/enterprise/app/views/api/v1/accounts/sla_policies/create.json.jbuilder new file mode 100644 index 000000000..5ecafcd2c --- /dev/null +++ b/enterprise/app/views/api/v1/accounts/sla_policies/create.json.jbuilder @@ -0,0 +1,3 @@ +json.payload do + json.partial! 'api/v1/models/sla_policy', formats: [:json], sla_policy: @sla_policy +end diff --git a/enterprise/app/views/api/v1/accounts/sla_policies/index.json.jbuilder b/enterprise/app/views/api/v1/accounts/sla_policies/index.json.jbuilder new file mode 100644 index 000000000..45534cc86 --- /dev/null +++ b/enterprise/app/views/api/v1/accounts/sla_policies/index.json.jbuilder @@ -0,0 +1,5 @@ +json.payload do + json.array! @sla_policies do |sla_policy| + json.partial! 'api/v1/models/sla_policy', formats: [:json], sla_policy: sla_policy + end +end diff --git a/enterprise/app/views/api/v1/accounts/sla_policies/show.json.jbuilder b/enterprise/app/views/api/v1/accounts/sla_policies/show.json.jbuilder new file mode 100644 index 000000000..5ecafcd2c --- /dev/null +++ b/enterprise/app/views/api/v1/accounts/sla_policies/show.json.jbuilder @@ -0,0 +1,3 @@ +json.payload do + json.partial! 'api/v1/models/sla_policy', formats: [:json], sla_policy: @sla_policy +end diff --git a/enterprise/app/views/api/v1/accounts/sla_policies/update.json.jbuilder b/enterprise/app/views/api/v1/accounts/sla_policies/update.json.jbuilder new file mode 100644 index 000000000..5ecafcd2c --- /dev/null +++ b/enterprise/app/views/api/v1/accounts/sla_policies/update.json.jbuilder @@ -0,0 +1,3 @@ +json.payload do + json.partial! 'api/v1/models/sla_policy', formats: [:json], sla_policy: @sla_policy +end diff --git a/enterprise/app/views/api/v1/models/_sla_policy.json.jbuilder b/enterprise/app/views/api/v1/models/_sla_policy.json.jbuilder new file mode 100644 index 000000000..cd03d50ad --- /dev/null +++ b/enterprise/app/views/api/v1/models/_sla_policy.json.jbuilder @@ -0,0 +1,5 @@ +json.id sla_policy.id +json.name sla_policy.name +json.frt_threshold sla_policy.frt_threshold +json.rt_threshold sla_policy.rt_threshold +json.only_during_business_hours sla_policy.only_during_business_hours diff --git a/spec/enterprise/controllers/api/v1/accounts/sla_policies_controller_spec.rb b/spec/enterprise/controllers/api/v1/accounts/sla_policies_controller_spec.rb new file mode 100644 index 000000000..99a3bf0f0 --- /dev/null +++ b/spec/enterprise/controllers/api/v1/accounts/sla_policies_controller_spec.rb @@ -0,0 +1,189 @@ +require 'rails_helper' + +RSpec.describe 'Enterprise SLA API', type: :request do + let(:account) { create(:account) } + let(:administrator) { create(:user, account: account, role: :administrator) } + let(:agent) { create(:user, account: account, role: :agent) } + + before do + create(:sla_policy, account: account, name: 'SLA 1') + end + + describe 'GET #index' do + context 'when it is an authenticated user' do + it 'returns all slas in the account' do + get "/api/v1/accounts/#{account.id}/sla_policies", + headers: administrator.create_new_auth_token + expect(response).to have_http_status(:success) + body = JSON.parse(response.body) + + expect(body['payload'][0]).to include('name' => 'SLA 1') + end + end + + context 'when the user is an agent' do + it 'returns slas in the account' do + get "/api/v1/accounts/#{account.id}/sla_policies", + headers: administrator.create_new_auth_token + expect(response).to have_http_status(:success) + body = JSON.parse(response.body) + + expect(body['payload'][0]).to include('name' => 'SLA 1') + end + end + + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/sla_policies" + + expect(response).to have_http_status(:unauthorized) + end + end + end + + describe 'GET #show' do + let(:sla_policy) { create(:sla_policy, account: account) } + + context 'when it is an authenticated user' do + it 'shows the sla' do + get "/api/v1/accounts/#{account.id}/sla_policies/#{sla_policy.id}", + headers: administrator.create_new_auth_token + + expect(response).to have_http_status(:success) + body = JSON.parse(response.body) + + expect(body['payload']).to include('name' => sla_policy.name) + end + end + + context 'when the user is an agent' do + it 'shows the sla details' do + get "/api/v1/accounts/#{account.id}/sla_policies/#{sla_policy.id}", + headers: agent.create_new_auth_token + + expect(response).to have_http_status(:success) + body = JSON.parse(response.body) + + expect(body['payload']).to include('name' => sla_policy.name) + end + end + + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/sla_policies" + + expect(response).to have_http_status(:unauthorized) + end + end + end + + describe 'POST #create' do + let(:valid_params) do + { sla_policy: { name: 'SLA 2', + frt_threshold: 1000, + rt_threshold: 1000, + only_during_business_hours: false } } + end + + context 'when it is an authenticated user' do + it 'creates the sla_policy' do + expect do + post "/api/v1/accounts/#{account.id}/sla_policies", params: valid_params, + headers: administrator.create_new_auth_token + end.to change(SlaPolicy, :count).by(1) + + expect(response).to have_http_status(:success) + body = JSON.parse(response.body) + + expect(body['payload']).to include('name' => 'SLA 2') + end + end + + context 'when the user is an agent' do + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/sla_policies", + params: valid_params, + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/sla_policies" + + expect(response).to have_http_status(:unauthorized) + end + end + end + + describe 'PUT #update' do + let(:sla_policy) { create(:sla_policy, account: account) } + + context 'when it is an authenticated user' do + it 'updates the sla_policy' do + put "/api/v1/accounts/#{account.id}/sla_policies/#{sla_policy.id}", + params: { sla_policy: { name: 'SLA 2' } }, + headers: administrator.create_new_auth_token + + expect(response).to have_http_status(:success) + body = JSON.parse(response.body) + + expect(body['payload']).to include('name' => 'SLA 2') + end + end + + context 'when the user is an agent' do + it 'returns unauthorized' do + put "/api/v1/accounts/#{account.id}/sla_policies/#{sla_policy.id}", + params: { sla_policy: { name: 'SLA 2' } }, + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + put "/api/v1/accounts/#{account.id}/sla_policies/#{sla_policy.id}" + + expect(response).to have_http_status(:unauthorized) + end + end + end + + describe 'DELETE #destroy' do + let(:sla_policy) { create(:sla_policy, account: account) } + + context 'when it is an authenticated user' do + it 'deletes the sla_policy' do + delete "/api/v1/accounts/#{account.id}/sla_policies/#{sla_policy.id}", + headers: administrator.create_new_auth_token + + expect(response).to have_http_status(:success) + expect(SlaPolicy.count).to eq(1) + end + end + + context 'when the user is an agent' do + it 'returns unauthorized' do + delete "/api/v1/accounts/#{account.id}/sla_policies/#{sla_policy.id}", + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + delete "/api/v1/accounts/#{account.id}/sla_policies/#{sla_policy.id}" + + expect(response).to have_http_status(:unauthorized) + end + end + end +end diff --git a/spec/enterprise/models/account_spec.rb b/spec/enterprise/models/account_spec.rb index 4c84e3465..af73da48f 100644 --- a/spec/enterprise/models/account_spec.rb +++ b/spec/enterprise/models/account_spec.rb @@ -3,6 +3,24 @@ require 'rails_helper' RSpec.describe Account do + include ActiveJob::TestHelper + + describe 'sla_policies' do + let!(:account) { create(:account) } + let!(:sla_policy) { create(:sla_policy, account: account) } + + it 'returns associated sla policies' do + expect(account.sla_policies).to eq([sla_policy]) + end + + it 'deletes associated sla policies' do + perform_enqueued_jobs do + account.destroy! + end + expect { sla_policy.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + end + describe 'usage_limits' do before do create(:installation_config, name: 'ACCOUNT_AGENTS_LIMIT', value: 20) diff --git a/spec/enterprise/models/sla_policy_spec.rb b/spec/enterprise/models/sla_policy_spec.rb new file mode 100644 index 000000000..6be1f4e45 --- /dev/null +++ b/spec/enterprise/models/sla_policy_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +RSpec.describe SlaPolicy, type: :model do + include ActiveJob::TestHelper + let(:account) { create(:account) } + let(:admin) { create(:user, account: account, role: :administrator) } + + describe 'validations' do + it { is_expected.to validate_presence_of(:name) } + end + + describe 'associations' do + it { is_expected.to belong_to(:account) } + end + + describe 'validates_factory' do + it 'creates valid sla policy object' do + sla_policy = create(:sla_policy) + expect(sla_policy.name).to eq 'sla_1' + end + end +end diff --git a/spec/factories/sla_policies.rb b/spec/factories/sla_policies.rb new file mode 100644 index 000000000..3f1d33b43 --- /dev/null +++ b/spec/factories/sla_policies.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :sla_policy do + account + name { 'sla_1' } + rt_threshold { 1000 } + frt_threshold { 2000 } + only_during_business_hours { false } + end +end From 020dcc4dc7accb9416a6bdd6fdfbfd44e6b11fea Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Thu, 11 May 2023 12:14:56 -0700 Subject: [PATCH 06/31] fix: Avoid audio URLs getting cached at the frontend (#7062) --- .../dashboard/components/widgets/conversation/Message.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/dashboard/components/widgets/conversation/Message.vue b/app/javascript/dashboard/components/widgets/conversation/Message.vue index 985c1ce11..6f20cac7c 100644 --- a/app/javascript/dashboard/components/widgets/conversation/Message.vue +++ b/app/javascript/dashboard/components/widgets/conversation/Message.vue @@ -59,7 +59,7 @@ controls class="skip-context-menu" > - + Date: Fri, 12 May 2023 14:05:22 +0530 Subject: [PATCH 07/31] feat: Refetch the latest messages on action cable reconnect in widget (#6996) --- .../api/v1/widget/messages_controller.rb | 5 +- app/javascript/widget/api/conversation.js | 4 +- app/javascript/widget/api/endPoints.js | 4 +- .../widget/api/specs/endPoints.spec.js | 23 ++ app/javascript/widget/helpers/actionCable.js | 16 ++ .../store/modules/conversation/actions.js | 34 +++ .../store/modules/conversation/index.js | 1 + .../store/modules/conversation/mutations.js | 12 + .../specs/conversation/actions.spec.js | 205 ++++++++++++++++++ .../specs/conversation/mutations.spec.js | 68 ++++++ 10 files changed, 366 insertions(+), 6 deletions(-) diff --git a/app/controllers/api/v1/widget/messages_controller.rb b/app/controllers/api/v1/widget/messages_controller.rb index 6287c94ee..76efb2c42 100644 --- a/app/controllers/api/v1/widget/messages_controller.rb +++ b/app/controllers/api/v1/widget/messages_controller.rb @@ -48,7 +48,8 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController def message_finder_params { filter_internal_messages: true, - before: permitted_params[:before] + before: permitted_params[:before], + after: permitted_params[:after] } end @@ -62,7 +63,7 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController def permitted_params # timestamp parameter is used in create conversation method - params.permit(:id, :before, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id]) + params.permit(:id, :before, :after, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id]) end def set_message diff --git a/app/javascript/widget/api/conversation.js b/app/javascript/widget/api/conversation.js index 78e4fe382..ded771316 100755 --- a/app/javascript/widget/api/conversation.js +++ b/app/javascript/widget/api/conversation.js @@ -16,8 +16,8 @@ const sendAttachmentAPI = async attachment => { return API.post(urlData.url, urlData.params); }; -const getMessagesAPI = async ({ before }) => { - const urlData = endPoints.getConversation({ before }); +const getMessagesAPI = async ({ before, after }) => { + const urlData = endPoints.getConversation({ before, after }); return API.get(urlData.url, { params: urlData.params }); }; diff --git a/app/javascript/widget/api/endPoints.js b/app/javascript/widget/api/endPoints.js index 298196a43..3839a8e82 100755 --- a/app/javascript/widget/api/endPoints.js +++ b/app/javascript/widget/api/endPoints.js @@ -57,9 +57,9 @@ const sendAttachment = ({ attachment }) => { }; }; -const getConversation = ({ before }) => ({ +const getConversation = ({ before, after }) => ({ url: `/api/v1/widget/messages${window.location.search}`, - params: { before }, + params: { before, after }, }); const updateMessage = id => ({ diff --git a/app/javascript/widget/api/specs/endPoints.spec.js b/app/javascript/widget/api/specs/endPoints.spec.js index 21ed98a5a..13e57a034 100644 --- a/app/javascript/widget/api/specs/endPoints.spec.js +++ b/app/javascript/widget/api/specs/endPoints.spec.js @@ -79,3 +79,26 @@ describe('#triggerCampaign', () => { spy.mockRestore(); }); }); + +describe('#getConversation', () => { + it('should returns correct payload', () => { + const spy = jest.spyOn(global, 'Date').mockImplementation(() => ({ + toString: () => 'mock date', + })); + const windowSpy = jest.spyOn(window, 'window', 'get'); + expect( + endPoints.getConversation({ + after: 123, + }) + ).toEqual({ + url: `/api/v1/widget/messages`, + params: { + after: 123, + before: undefined, + }, + }); + windowSpy.mockRestore(); + + spy.mockRestore(); + }); +}); diff --git a/app/javascript/widget/helpers/actionCable.js b/app/javascript/widget/helpers/actionCable.js index 40f95c342..6d7af376c 100644 --- a/app/javascript/widget/helpers/actionCable.js +++ b/app/javascript/widget/helpers/actionCable.js @@ -25,6 +25,22 @@ class ActionCableConnector extends BaseActionCableConnector { }; } + onDisconnected = () => { + this.setLastMessageId(); + }; + + onReconnect = () => { + this.syncLatestMessages(); + }; + + setLastMessageId = () => { + this.app.$store.dispatch('conversation/setLastMessageId'); + }; + + syncLatestMessages = () => { + this.app.$store.dispatch('conversation/syncLatestMessages'); + }; + onStatusChange = data => { if (data.status === 'resolved') { this.app.$store.dispatch('campaign/resetCampaign'); diff --git a/app/javascript/widget/store/modules/conversation/actions.js b/app/javascript/widget/store/modules/conversation/actions.js index ec29ea13c..526e1d5aa 100644 --- a/app/javascript/widget/store/modules/conversation/actions.js +++ b/app/javascript/widget/store/modules/conversation/actions.js @@ -52,6 +52,10 @@ export const actions = { } }, + setLastMessageId: async ({ commit }) => { + commit('setLastMessageId'); + }, + sendAttachment: async ({ commit }, params) => { const { attachment: { thumbUrl, fileType }, @@ -99,6 +103,36 @@ export const actions = { } }, + syncLatestMessages: async ({ state, commit }) => { + try { + const { lastMessageId, conversations } = state; + + const { + data: { payload, meta }, + } = await getMessagesAPI({ after: lastMessageId }); + + const { contact_last_seen_at: lastSeen } = meta; + const formattedMessages = getNonDeletedMessages({ messages: payload }); + const missingMessages = formattedMessages.filter( + message => conversations?.[message.id] === undefined + ); + if (!missingMessages.length) return; + missingMessages.forEach(message => { + conversations[message.id] = message; + }); + // Sort conversation messages by created_at + const updatedConversation = Object.fromEntries( + Object.entries(conversations).sort( + (a, b) => a[1].created_at - b[1].created_at + ) + ); + commit('conversation/setMetaUserLastSeenAt', lastSeen, { root: true }); + commit('setMissingMessagesInConversation', updatedConversation); + } catch (error) { + // IgnoreError + } + }, + clearConversations: ({ commit }) => { commit('clearConversations'); }, diff --git a/app/javascript/widget/store/modules/conversation/index.js b/app/javascript/widget/store/modules/conversation/index.js index d8bf30d04..9869b6a87 100755 --- a/app/javascript/widget/store/modules/conversation/index.js +++ b/app/javascript/widget/store/modules/conversation/index.js @@ -13,6 +13,7 @@ const state = { isAgentTyping: false, isCreating: false, }, + lastMessageId: null, }; export default { diff --git a/app/javascript/widget/store/modules/conversation/mutations.js b/app/javascript/widget/store/modules/conversation/mutations.js index ca6dafada..04b4987ff 100644 --- a/app/javascript/widget/store/modules/conversation/mutations.js +++ b/app/javascript/widget/store/modules/conversation/mutations.js @@ -62,6 +62,10 @@ export const mutations = { payload.map(message => Vue.set($state.conversations, message.id, message)); }, + setMissingMessagesInConversation($state, payload) { + Vue.set($state, 'conversation', payload); + }, + updateMessage($state, { id, content_attributes }) { $state.conversations[id] = { ...$state.conversations[id], @@ -94,4 +98,12 @@ export const mutations = { setMetaUserLastSeenAt($state, lastSeen) { $state.meta.userLastSeenAt = lastSeen; }, + + setLastMessageId($state) { + const { conversations } = $state; + const lastMessage = Object.values(conversations).pop(); + if (!lastMessage) return; + const { id } = lastMessage; + $state.lastMessageId = id; + }, }; diff --git a/app/javascript/widget/store/modules/specs/conversation/actions.spec.js b/app/javascript/widget/store/modules/specs/conversation/actions.spec.js index 82e03d38a..81d0d7694 100644 --- a/app/javascript/widget/store/modules/specs/conversation/actions.spec.js +++ b/app/javascript/widget/store/modules/specs/conversation/actions.spec.js @@ -217,4 +217,209 @@ describe('#actions', () => { ]); }); }); + + describe('#syncLatestMessages', () => { + it('latest message should append to end of list', async () => { + const state = { + uiFlags: { allMessagesLoaded: false }, + conversations: { + '454': { + id: 454, + content: 'hi', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682244355, // Sunday, 23 April 2023 10:05:55 + conversation_id: 20, + }, + '463': { + id: 463, + content: 'ss', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682490729, // Wednesday, 26 April 2023 06:32:09 + conversation_id: 20, + }, + }, + lastMessageId: 463, + }; + API.get.mockResolvedValue({ + data: { + payload: [ + { + id: 465, + content: 'hi', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682504326, // Wednesday, 26 April 2023 10:18:46 + conversation_id: 20, + }, + ], + meta: { + contact_last_seen_at: 1466424490, + }, + }, + }); + await actions.syncLatestMessages({ state, commit }, {}); + expect(commit.mock.calls).toEqual([ + ['conversation/setMetaUserLastSeenAt', 1466424490, { root: true }], + [ + 'setMissingMessagesInConversation', + + { + '454': { + id: 454, + content: 'hi', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682244355, + conversation_id: 20, + }, + '463': { + id: 463, + content: 'ss', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682490729, + conversation_id: 20, + }, + '465': { + id: 465, + content: 'hi', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682504326, + conversation_id: 20, + }, + }, + ], + ]); + }); + + it('old message should insert to exact position', async () => { + const state = { + uiFlags: { allMessagesLoaded: false }, + conversations: { + '454': { + id: 454, + content: 'hi', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682244355, // Sunday, 23 April 2023 10:05:55 + conversation_id: 20, + }, + '463': { + id: 463, + content: 'ss', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682490729, // Wednesday, 26 April 2023 06:32:09 + conversation_id: 20, + }, + }, + lastMessageId: 463, + }; + API.get.mockResolvedValue({ + data: { + payload: [ + { + id: 460, + content: 'Hi how are you', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682417926, // Tuesday, 25 April 2023 10:18:46 + conversation_id: 20, + }, + ], + meta: { + contact_last_seen_at: 14664223490, + }, + }, + }); + await actions.syncLatestMessages({ state, commit }, {}); + + expect(commit.mock.calls).toEqual([ + ['conversation/setMetaUserLastSeenAt', 14664223490, { root: true }], + [ + 'setMissingMessagesInConversation', + + { + '454': { + id: 454, + content: 'hi', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682244355, + conversation_id: 20, + }, + '460': { + id: 460, + content: 'Hi how are you', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682417926, + conversation_id: 20, + }, + '463': { + id: 463, + content: 'ss', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682490729, + conversation_id: 20, + }, + }, + ], + ]); + }); + + it('abort syncing if there is no missing messages ', async () => { + const state = { + uiFlags: { allMessagesLoaded: false }, + conversation: { + '454': { + id: 454, + content: 'hi', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682244355, // Sunday, 23 April 2023 10:05:55 + conversation_id: 20, + }, + '463': { + id: 463, + content: 'ss', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682490729, // Wednesday, 26 April 2023 06:32:09 + conversation_id: 20, + }, + }, + lastMessageId: 463, + }; + API.get.mockResolvedValue({ + data: { + payload: [], + meta: { + contact_last_seen_at: 14664223490, + }, + }, + }); + await actions.syncLatestMessages({ state, commit }, {}); + + expect(commit.mock.calls).toEqual([]); + }); + }); }); diff --git a/app/javascript/widget/store/modules/specs/conversation/mutations.spec.js b/app/javascript/widget/store/modules/specs/conversation/mutations.spec.js index 96d2e8da8..0b96c6e20 100644 --- a/app/javascript/widget/store/modules/specs/conversation/mutations.spec.js +++ b/app/javascript/widget/store/modules/specs/conversation/mutations.spec.js @@ -183,4 +183,72 @@ describe('#mutations', () => { expect(state.conversations).toEqual({}); }); }); + + describe('#setMissingMessages', () => { + it('sets messages if payload is not empty', () => { + const state = { + uiFlags: { allMessagesLoaded: false }, + conversations: { + '454': { + id: 454, + content: 'hi', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682432667, + conversation_id: 20, + }, + '464': { + id: 464, + content: 'hey will be back soon', + message_type: 3, + content_type: 'text', + content_attributes: {}, + created_at: 1682490729, + conversation_id: 20, + }, + }, + }; + mutations.setMessagesInConversation(state, [ + { + id: 455, + content: 'Hey billowing-grass-423 how are you?', + message_type: 3, + content_type: 'text', + content_attributes: {}, + created_at: 1682432667, + conversation_id: 20, + }, + ]); + expect(state.conversations).toEqual({ + '454': { + id: 454, + content: 'hi', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682432667, + conversation_id: 20, + }, + '455': { + id: 455, + content: 'Hey billowing-grass-423 how are you?', + message_type: 3, + content_type: 'text', + content_attributes: {}, + created_at: 1682432667, + conversation_id: 20, + }, + '464': { + id: 464, + content: 'hey will be back soon', + message_type: 3, + content_type: 'text', + content_attributes: {}, + created_at: 1682490729, + conversation_id: 20, + }, + }); + }); + }); }); From 2c3160cfee7cdeff1106691a1a3dcdc8ed13c689 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Fri, 12 May 2023 15:48:06 +0530 Subject: [PATCH 08/31] feat: API to list all attachments for a conversation (#7059) Fixes: https://linear.app/chatwoot/issue/CW-1678/api-to-list-all-attachments-for-a-conversation --- .../v1/accounts/conversations_controller.rb | 4 ++ app/models/conversation.rb | 1 + .../conversations/attachments.json.jbuilder | 1 + config/routes.rb | 1 + .../accounts/conversations_controller_spec.rb | 49 +++++++++++++++++++ spec/factories/messages.rb | 7 +++ 6 files changed, 63 insertions(+) create mode 100644 app/views/api/v1/accounts/conversations/attachments.json.jbuilder diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index 6e580a497..ebd673d6f 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -22,6 +22,10 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro @conversations_count = result[:count] end + def attachments + @attachments = @conversation.attachments + end + def create ActiveRecord::Base.transaction do @conversation = ConversationBuilder.new(params: params, contact_inbox: @contact_inbox).perform diff --git a/app/models/conversation.rb b/app/models/conversation.rb index e01fb3863..becb0a639 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -95,6 +95,7 @@ class Conversation < ApplicationRecord has_one :csat_survey_response, dependent: :destroy_async has_many :conversation_participants, dependent: :destroy_async has_many :notifications, as: :primary_actor, dependent: :destroy_async + has_many :attachments, through: :messages before_save :ensure_snooze_until_reset before_create :mark_conversation_pending_if_bot diff --git a/app/views/api/v1/accounts/conversations/attachments.json.jbuilder b/app/views/api/v1/accounts/conversations/attachments.json.jbuilder new file mode 100644 index 000000000..e31980ea2 --- /dev/null +++ b/app/views/api/v1/accounts/conversations/attachments.json.jbuilder @@ -0,0 +1 @@ +json.payload @attachments.map(&:push_event_data) diff --git a/config/routes.rb b/config/routes.rb index bdad7c1bf..650f8a64c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -97,6 +97,7 @@ Rails.application.routes.draw do post :update_last_seen post :unread post :custom_attributes + get :attachments end end diff --git a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb index 9689bdd4d..bf73cd0ce 100644 --- a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb @@ -731,4 +731,53 @@ RSpec.describe 'Conversations API', type: :request do end end end + + describe 'GET /api/v1/accounts/{account.id}/conversations/:id/attachments' do + let(:conversation) { create(:conversation, account: account) } + + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/attachments" + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + let(:agent) { create(:user, account: account, role: :agent) } + let(:administrator) { create(:user, account: account, role: :administrator) } + + before do + create(:message, :with_attachment, conversation: conversation, account: account, inbox: conversation.inbox, message_type: 'incoming') + end + + it 'does not return the attachments if you do not have access to it' do + get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/attachments", + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unauthorized) + end + + it 'return the attachments if you are an administrator' do + get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/attachments", + headers: administrator.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + response_body = JSON.parse(response.body) + expect(response_body['payload'].first['file_type']).to eq('image') + end + + it 'return the attachments if you are an agent with access to inbox' do + get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/attachments", + headers: administrator.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + response_body = JSON.parse(response.body) + expect(response_body['payload'].length).to eq(1) + end + end + end end diff --git a/spec/factories/messages.rb b/spec/factories/messages.rb index e14a8000d..3e51f2b35 100644 --- a/spec/factories/messages.rb +++ b/spec/factories/messages.rb @@ -20,6 +20,13 @@ FactoryBot.define do end end + trait :with_attachment do + after(:build) do |message| + attachment = message.attachments.new(account_id: message.account_id, file_type: :image) + attachment.file.attach(io: File.open(Rails.root.join('spec/assets/avatar.png')), filename: 'avatar.png', content_type: 'image/png') + end + end + after(:build) do |message| message.sender ||= message.outgoing? ? create(:user, account: message.account) : create(:contact, account: message.account) message.inbox ||= message.conversation&.inbox || create(:inbox, account: message.account) From abdf00d2cfc3e26ccef8b892b73edf46ed3a56a1 Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Fri, 12 May 2023 16:39:18 +0530 Subject: [PATCH 09/31] fixes: Editor adding slash char while creating block quotes [cw-1505] (#7069) * fixes: Editor adding slash char while creating block quotes [cw-1505] * Update with latest package --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index cd0c7560a..6cdb92044 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2180,7 +2180,7 @@ "@chatwoot/prosemirror-schema@https://github.com/chatwoot/prosemirror-schema.git": version "1.0.0" - resolved "https://github.com/chatwoot/prosemirror-schema.git#3306cdb220797090ce41ea607174e940330177e1" + resolved "https://github.com/chatwoot/prosemirror-schema.git#b937c3cb44210b7d251daf766988758975befec7" dependencies: prosemirror-commands "^1.1.4" prosemirror-dropcursor "^1.3.2" From 198cd9b28d5137e9069f749e6d9b7f9b3809fba3 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Fri, 12 May 2023 19:25:51 +0530 Subject: [PATCH 10/31] feat: Show next available day/hour and minutes on widget (#6902) * feat: Show next available hour and minutes on widget * chore: Adds spec * chore: Show days * chore: Code clean up * chore: Review fixes * chore: Minor fixes * chore: Review suggestion fixes * chore: Minor fixes * Added timezone to widget payload * chore: Adds time zone * chore: Review fixes * chore: Adds comments * chore: Rounded up min with nearest multiple of 5 * chore: Review fixes * chore: Review fixes * chore: Review fixes * chore: Review fixes * chore: Fix specs * chore: Review fixes * chore: Fix specs * chore: Review fixes * chore: Moved day names to i18n * chore: Review fixes * chore: Fix specs --------- Co-authored-by: Tejaswini Chile Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Co-authored-by: Shivam Mishra --- app/javascript/shared/helpers/DateHelper.js | 7 + .../shared/helpers/specs/DateHelper.spec.js | 14 + .../widget/components/ChatHeader.vue | 8 +- .../widget/components/TeamAvailability.vue | 14 +- app/javascript/widget/i18n/locale/en.json | 12 +- app/javascript/widget/mixins/availability.js | 11 + .../widget/mixins/nextAvailabilityTime.js | 242 ++++++++ .../mixins/specs/nextAvailabilityTime.spec.js | 544 ++++++++++++++++++ app/views/widgets/show.html.erb | 1 + 9 files changed, 834 insertions(+), 19 deletions(-) create mode 100644 app/javascript/widget/mixins/nextAvailabilityTime.js create mode 100644 app/javascript/widget/mixins/specs/nextAvailabilityTime.spec.js diff --git a/app/javascript/shared/helpers/DateHelper.js b/app/javascript/shared/helpers/DateHelper.js index 15638c5a8..c8b0f231b 100644 --- a/app/javascript/shared/helpers/DateHelper.js +++ b/app/javascript/shared/helpers/DateHelper.js @@ -30,3 +30,10 @@ export const isTimeAfter = (h1, m1, h2, m2) => { return true; }; + +export const generateRelativeTime = (value, unit, languageCode) => { + const rtf = new Intl.RelativeTimeFormat(languageCode, { + numeric: 'auto', + }); + return rtf.format(value, unit); +}; diff --git a/app/javascript/shared/helpers/specs/DateHelper.spec.js b/app/javascript/shared/helpers/specs/DateHelper.spec.js index c67c1a27b..030910a3d 100644 --- a/app/javascript/shared/helpers/specs/DateHelper.spec.js +++ b/app/javascript/shared/helpers/specs/DateHelper.spec.js @@ -3,6 +3,7 @@ import { formatUnixDate, formatDigitToString, isTimeAfter, + generateRelativeTime, } from '../DateHelper'; describe('#DateHelper', () => { @@ -62,3 +63,16 @@ describe('#isTimeAfter', () => { expect(isTimeAfter(11, 59, 12, 0)).toEqual(false); }); }); + +describe('#generateRelativeTime', () => { + it('should return correct relative time', () => { + expect(generateRelativeTime(-1, 'day', 'en')).toEqual('yesterday'); + expect(generateRelativeTime(1, 'day', 'en')).toEqual('tomorrow'); + expect(generateRelativeTime(1, 'hour', 'en')).toEqual('in 1 hour'); + expect(generateRelativeTime(-1, 'hour', 'en')).toEqual('1 hour ago'); + expect(generateRelativeTime(1, 'minute', 'en')).toEqual('in 1 minute'); + expect(generateRelativeTime(-1, 'minute', 'en')).toEqual('1 minute ago'); + expect(generateRelativeTime(1, 'second', 'en')).toEqual('in 1 second'); + expect(generateRelativeTime(-1, 'second', 'en')).toEqual('1 second ago'); + }); +}); diff --git a/app/javascript/widget/components/ChatHeader.vue b/app/javascript/widget/components/ChatHeader.vue index 906c98d14..f6a8aecd8 100644 --- a/app/javascript/widget/components/ChatHeader.vue +++ b/app/javascript/widget/components/ChatHeader.vue @@ -46,6 +46,7 @@ import { mapGetters } from 'vuex'; import availabilityMixin from 'widget/mixins/availability'; +import nextAvailabilityTime from 'widget/mixins/nextAvailabilityTime'; import FluentIcon from 'shared/components/FluentIcon/Index.vue'; import HeaderActions from './HeaderActions'; import routerMixin from 'widget/mixins/routerMixin'; @@ -57,7 +58,7 @@ export default { FluentIcon, HeaderActions, }, - mixins: [availabilityMixin, routerMixin, darkMixin], + mixins: [nextAvailabilityTime, availabilityMixin, routerMixin, darkMixin], props: { avatarUrl: { type: String, @@ -93,11 +94,6 @@ export default { } return anyAgentOnline; }, - replyWaitMessage() { - return this.isOnline - ? this.replyTimeStatus - : this.$t('TEAM_AVAILABILITY.OFFLINE'); - }, }, methods: { onBackButtonClick() { diff --git a/app/javascript/widget/components/TeamAvailability.vue b/app/javascript/widget/components/TeamAvailability.vue index a3a6b0c02..8305c7a4e 100644 --- a/app/javascript/widget/components/TeamAvailability.vue +++ b/app/javascript/widget/components/TeamAvailability.vue @@ -35,6 +35,7 @@ diff --git a/app/javascript/portal/portalHelpers.js b/app/javascript/portal/portalHelpers.js index 647059b73..27adad3d5 100644 --- a/app/javascript/portal/portalHelpers.js +++ b/app/javascript/portal/portalHelpers.js @@ -1,13 +1,79 @@ -export const navigateToLocalePage = () => { - const allLocaleSwitcher = document.querySelector('.locale-switcher'); +import slugifyWithCounter from '@sindresorhus/slugify'; +import Vue from 'vue'; - if (!allLocaleSwitcher) { - return false; - } +import PublicArticleSearch from './components/PublicArticleSearch.vue'; +import TableOfContents from './components/TableOfContents.vue'; - const { portalSlug } = allLocaleSwitcher.dataset; - allLocaleSwitcher.addEventListener('change', event => { - window.location = `/hc/${portalSlug}/${event.target.value}/`; +export const getHeadingsfromTheArticle = () => { + const rows = []; + const articleElement = document.getElementById('cw-article-content'); + articleElement.querySelectorAll('h1, h2, h3').forEach(element => { + const slug = slugifyWithCounter(element.innerText); + element.id = slug; + element.className = 'scroll-mt-24 heading'; + element.innerHTML += ``; + rows.push({ + slug, + title: element.innerText, + tag: element.tagName.toLowerCase(), + }); }); - return false; + return rows; +}; + +export const InitializationHelpers = { + navigateToLocalePage: () => { + const allLocaleSwitcher = document.querySelector('.locale-switcher'); + + if (!allLocaleSwitcher) { + return false; + } + + const { portalSlug } = allLocaleSwitcher.dataset; + allLocaleSwitcher.addEventListener('change', event => { + window.location = `/hc/${portalSlug}/${event.target.value}/`; + }); + return false; + }, + + initalizeSearch: () => { + const isSearchContainerAvailable = document.querySelector('#search-wrap'); + if (isSearchContainerAvailable) { + new Vue({ + components: { PublicArticleSearch }, + template: '', + }).$mount('#search-wrap'); + } + }, + + initializeTableOfContents: () => { + const isOnArticlePage = document.querySelector('#cw-hc-toc'); + if (isOnArticlePage) { + new Vue({ + components: { TableOfContents }, + data: { rows: getHeadingsfromTheArticle() }, + template: '', + }).$mount('#cw-hc-toc'); + } + }, + + initialize: () => { + InitializationHelpers.navigateToLocalePage(); + InitializationHelpers.initalizeSearch(); + InitializationHelpers.initializeTableOfContents(); + }, + + onLoad: () => { + InitializationHelpers.initialize(); + if (window.location.hash) { + if ('scrollRestoration' in window.history) { + window.history.scrollRestoration = 'manual'; + } + + const a = document.createElement('a'); + a.href = window.location.hash; + a['data-turbolinks'] = false; + a.click(); + } + }, }; diff --git a/app/javascript/portal/specs/portal.spec.js b/app/javascript/portal/specs/portal.spec.js index 31dc06890..cd4347bad 100644 --- a/app/javascript/portal/specs/portal.spec.js +++ b/app/javascript/portal/specs/portal.spec.js @@ -1,4 +1,4 @@ -import { navigateToLocalePage } from '../portalHelpers'; +import { InitializationHelpers } from '../portalHelpers'; describe('#navigateToLocalePage', () => { it('returns correct cookie name', () => { @@ -14,7 +14,7 @@ describe('#navigateToLocalePage', () => { callback({ target: { value: 1 } }); }); - navigateToLocalePage(); + InitializationHelpers.navigateToLocalePage(); expect(allLocaleSwitcher.addEventListener).toBeCalledWith( 'change', expect.any(Function) diff --git a/app/views/layouts/portal.html.erb b/app/views/layouts/portal.html.erb index 071fde9ee..8f1dda7ae 100644 --- a/app/views/layouts/portal.html.erb +++ b/app/views/layouts/portal.html.erb @@ -44,8 +44,9 @@ By default, it renders: searchPlaceholder: '<%= I18n.t('public_portal.search.search_placeholder') %>', emptyPlaceholder: '<%= I18n.t('public_portal.search.empty_placeholder') %>', loadingPlaceholder: '<%= I18n.t('public_portal.search.loading_placeholder') %>', - resultsTitle: '<%= I18n.t('public_portal.search.results_title') %>' - } + resultsTitle: '<%= I18n.t('public_portal.search.results_title') %>', + }, + tocHeader: '<%= I18n.t('public_portal.toc_header') %>' }; diff --git a/app/views/public/api/v1/portals/articles/show.html.erb b/app/views/public/api/v1/portals/articles/show.html.erb index bea1a20e8..20f14e787 100644 --- a/app/views/public/api/v1/portals/articles/show.html.erb +++ b/app/views/public/api/v1/portals/articles/show.html.erb @@ -35,9 +35,9 @@
<% if @article.author&.avatar_url&.present? %> - <%= @article.author.display_name %> + <%= @article.author.display_name %> <% end %> -
+
<%= @article.author.available_name %>

<%= I18n.t('public_portal.common.last_updated_on', last_updated_on: @article.updated_at.strftime("%b %d, %Y")) %> @@ -46,10 +46,9 @@

-
-
-
-

<%= @parsed_content %>

-
+
+
+ <%= @parsed_content %>
+
diff --git a/config/locales/en.yml b/config/locales/en.yml index 0a9026554..2a2437566 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -196,6 +196,7 @@ en: empty_placeholder: No results found. loading_placeholder: Searching... results_title: Search results + toc_header: 'On this page' hero: sub_title: Search for the articles here or browse the categories below. common: diff --git a/package.json b/package.json index 9b739c030..24c61216d 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@rails/webpacker": "5.4.4", "@sentry/tracing": "^6.19.7", "@sentry/vue": "^6.19.7", + "@sindresorhus/slugify": "1.1.0", "@tailwindcss/typography": "0.2.0", "activestorage": "^5.2.6", "axios": "^0.21.2", @@ -133,13 +134,6 @@ "pre-push": "sh bin/validate_push" } }, - "jest": { - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "text" - ] - }, "lint-staged": { "app/**/*.{js,vue}": [ "eslint --fix", diff --git a/tailwind.config.js b/tailwind.config.js index 1076ae315..0dc013c8e 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -97,7 +97,6 @@ module.exports = { }, }, }, - variants: {}, plugins: [ // eslint-disable-next-line require('@tailwindcss/typography'), diff --git a/yarn.lock b/yarn.lock index 6cdb92044..323c690c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2887,6 +2887,22 @@ "@sentry/utils" "6.19.7" tslib "^1.9.3" +"@sindresorhus/slugify@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/slugify/-/slugify-1.1.0.tgz#2f195365d9b953384305b62664b44b4036c49430" + integrity sha512-ujZRbmmizX26yS/HnB3P9QNlNa4+UvHh+rIse3RbOXLp8yl6n1TxB4t7NHggtVgS8QmmOtzXo48kCxZGACpkPw== + dependencies: + "@sindresorhus/transliterate" "^0.1.1" + escape-string-regexp "^4.0.0" + +"@sindresorhus/transliterate@^0.1.1": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@sindresorhus/transliterate/-/transliterate-0.1.2.tgz#ffce368271d153550e87de81486004f2637425af" + integrity sha512-5/kmIOY9FF32nicXH+5yLNTX4NJ4atl7jRgqAJuIn/iyDFXBktOKDxCvyGE/EzmF4ngSUvjXxQUQlQiZ5lfw+w== + dependencies: + escape-string-regexp "^2.0.0" + lodash.deburr "^4.1.0" + "@sinonjs/commons@^1.7.0": version "1.8.3" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" @@ -8468,6 +8484,11 @@ escape-string-regexp@^2.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + escodegen@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" @@ -12048,6 +12069,11 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= +lodash.deburr@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/lodash.deburr/-/lodash.deburr-4.1.0.tgz#ddb1bbb3ef07458c0177ba07de14422cb033ff9b" + integrity sha512-m/M1U1f3ddMCs6Hq2tAsYThTBDaAKFDX3dwDo97GEYzamXi9SqUpjWi/Rrj/gf3X2n8ktwgZrlP1z6E3v/IExQ== + lodash.get@^4.0: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" From 3f39b7ad1f104ec03dac12e517d1912618abfe5c Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Mon, 15 May 2023 19:05:07 -0700 Subject: [PATCH 25/31] fix: Avoid styles getting purged on prod build (#7086) --- app/javascript/portal/application.scss | 6 +++++- app/javascript/portal/portalHelpers.js | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/javascript/portal/application.scss b/app/javascript/portal/application.scss index 8a1b7461b..866d21c14 100644 --- a/app/javascript/portal/application.scss +++ b/app/javascript/portal/application.scss @@ -28,8 +28,12 @@ body { } .heading { + .permalink { + visibility: hidden; + } + &:hover { - a { + .permalink { visibility: visible; } } diff --git a/app/javascript/portal/portalHelpers.js b/app/javascript/portal/portalHelpers.js index 27adad3d5..b5f892398 100644 --- a/app/javascript/portal/portalHelpers.js +++ b/app/javascript/portal/portalHelpers.js @@ -11,7 +11,7 @@ export const getHeadingsfromTheArticle = () => { const slug = slugifyWithCounter(element.innerText); element.id = slug; element.className = 'scroll-mt-24 heading'; - element.innerHTML += ``; + element.innerHTML += ``; rows.push({ slug, title: element.innerText, From 7b8bcaba111c03e44dc1fe7607b2ca4db5dd5b07 Mon Sep 17 00:00:00 2001 From: Sojan Date: Tue, 16 May 2023 12:33:16 +0530 Subject: [PATCH 26/31] Bump version to 2.17.0 --- config/app.yml | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/app.yml b/config/app.yml index d142137c1..1096df605 100644 --- a/config/app.yml +++ b/config/app.yml @@ -1,5 +1,5 @@ shared: &shared - version: '2.16.1' + version: '2.17.0' development: <<: *shared diff --git a/package.json b/package.json index 24c61216d..56713231c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@chatwoot/chatwoot", - "version": "2.16.1", + "version": "2.17.0", "license": "MIT", "scripts": { "eslint": "eslint app/**/*.{js,vue}", From 49ef4e54cae187d3fae77eb85b009afcb859048a Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Tue, 16 May 2023 16:57:25 -0700 Subject: [PATCH 27/31] feat: Add cmd/ctrl click open on the conversation cards (#7100) --- .../widgets/conversation/ConversationCard.vue | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue index 83c40e027..c646b9b6f 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue @@ -9,7 +9,7 @@ }" @mouseenter="onCardHover" @mouseleave="onCardLeave" - @click="cardClick(chat)" + @click="onCardClick" @contextmenu="openContextMenu($event)" >
From aea9470b6ae682133c62e3305d2c9fe68c17933a Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 18 May 2023 15:06:18 +0530 Subject: [PATCH 29/31] fix: RTL issue for basic filter dropdown (#7118) --- app/javascript/dashboard/assets/scss/_rtl.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/javascript/dashboard/assets/scss/_rtl.scss b/app/javascript/dashboard/assets/scss/_rtl.scss index 0ed2fe2f4..4f7efaab9 100644 --- a/app/javascript/dashboard/assets/scss/_rtl.scss +++ b/app/javascript/dashboard/assets/scss/_rtl.scss @@ -261,6 +261,12 @@ } } + // Basic filter dropdown + .basic-filter { + left: 0; + right: unset; + } + // Card label .label-container { .label { From a25179c342fe3c6a2fdcabc9ab9f0284a3a448a4 Mon Sep 17 00:00:00 2001 From: Clairton Rodrigo Heinzen Date: Thu, 18 May 2023 06:52:01 -0300 Subject: [PATCH 30/31] fix: Instagram events job NoMethodError: undefined method for nil:NilClass (#7105) - Sometimes tag entry does not have messaging or standby and cause a error --- app/jobs/webhooks/instagram_events_job.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/webhooks/instagram_events_job.rb b/app/jobs/webhooks/instagram_events_job.rb index ad02a3c18..ef4dd0247 100644 --- a/app/jobs/webhooks/instagram_events_job.rb +++ b/app/jobs/webhooks/instagram_events_job.rb @@ -31,6 +31,6 @@ class Webhooks::InstagramEventsJob < ApplicationJob end def messages(entry) - (entry[:messaging].presence || entry[:standby]) + (entry[:messaging].presence || entry[:standby] || []) end end From 590ce788b99c7634936af07bf999c05abadb0416 Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Thu, 18 May 2023 16:53:47 +0530 Subject: [PATCH 31/31] fix: update linux script(cwctl) to account for rails7 upgrade (#7106) * chore: modify cwctl to fetch latest redis version on new installations * fix: add libvips for activestorage imageprocessing support * chore: update cwctl version * feat: upgrade redis and install libvps for existing installations --- VERSION_CW | 2 +- VERSION_CWCTL | 2 +- deployment/setup_20.04.sh | 30 +++++++++++++++++++++++++++--- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/VERSION_CW b/VERSION_CW index ccbccc3dc..d76bd2ba3 100644 --- a/VERSION_CW +++ b/VERSION_CW @@ -1 +1 @@ -2.2.0 +2.17.0 diff --git a/VERSION_CWCTL b/VERSION_CWCTL index 7ec1d6db4..276cbf9e2 100644 --- a/VERSION_CWCTL +++ b/VERSION_CWCTL @@ -1 +1 @@ -2.1.0 +2.3.0 diff --git a/deployment/setup_20.04.sh b/deployment/setup_20.04.sh index 20cc672df..8cd0a4e1a 100644 --- a/deployment/setup_20.04.sh +++ b/deployment/setup_20.04.sh @@ -2,7 +2,7 @@ # Description: Install and manage a Chatwoot installation. # OS: Ubuntu 20.04 LTS -# Script Version: 2.2.0 +# Script Version: 2.3.0 # Run this script as root set -eu -o errexit -o pipefail -o noclobber -o nounset @@ -19,7 +19,7 @@ fi # option --output/-o requires 1 argument LONGOPTS=console,debug,help,install,Install:,logs:,restart,ssl,upgrade,webserver,version OPTIONS=cdhiI:l:rsuwv -CWCTL_VERSION="2.2.0" +CWCTL_VERSION="2.3.0" pg_pass=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 15 ; echo '') # if user does not specify an option @@ -175,6 +175,8 @@ function install_dependencies() { curl -sL https://deb.nodesource.com/setup_16.x | bash - curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list + curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg + echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list apt update apt install -y \ @@ -183,7 +185,8 @@ function install_dependencies() { libssl-dev libyaml-dev libreadline-dev gnupg2 \ postgresql-client redis-tools \ nodejs yarn patch ruby-dev zlib1g-dev liblzma-dev \ - libgmp-dev libncurses5-dev libffi-dev libgdbm6 libgdbm-dev sudo + libgmp-dev libncurses5-dev libffi-dev libgdbm6 libgdbm-dev sudo \ + libvips } ############################################################################## @@ -708,6 +711,26 @@ function upgrade_prereq() { EOF } +############################################################################## +# Update redis to v7+ for Rails 7 support(-u/--upgrade) +# and install libvips for image processing support in Rails 7 +# Globals: +# None +# Arguments: +# None +# Outputs: +# None +############################################################################## +function upgrade_redis() { + echo "Upgrading Redis to v7+ for Rails 7 support(Chatwoot v2.17+)" + curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg + echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list + apt update -y + apt upgrade redis-server -y + apt install libvips -y +} + + ############################################################################## # Upgrade an existing installation to latest stable version(-u/--upgrade) # Globals: @@ -722,6 +745,7 @@ function upgrade() { echo "Upgrading Chatwoot to v$CW_VERSION" sleep 3 upgrade_prereq + upgrade_redis sudo -i -u chatwoot << "EOF" # Navigate to the Chatwoot directory