From 9917cb42730f3f61d51f2c0465bf0005d314f8cf Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 26 Mar 2024 07:17:08 +0530 Subject: [PATCH 1/7] fix: Convert `cached_label_list` to text (#9143) --- app/models/conversation.rb | 2 +- ...71629_convert_cached_label_list_to_text.rb | 32 +++++++++++++++++++ db/schema.rb | 4 +-- 3 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20240322071629_convert_cached_label_list_to_text.rb diff --git a/app/models/conversation.rb b/app/models/conversation.rb index 72518f250..513c993da 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -6,7 +6,7 @@ # additional_attributes :jsonb # agent_last_seen_at :datetime # assignee_last_seen_at :datetime -# cached_label_list :string +# cached_label_list :text # contact_last_seen_at :datetime # custom_attributes :jsonb # first_reply_created_at :datetime diff --git a/db/migrate/20240322071629_convert_cached_label_list_to_text.rb b/db/migrate/20240322071629_convert_cached_label_list_to_text.rb new file mode 100644 index 000000000..91dfb428a --- /dev/null +++ b/db/migrate/20240322071629_convert_cached_label_list_to_text.rb @@ -0,0 +1,32 @@ +class ConvertCachedLabelListToText < ActiveRecord::Migration[7.0] + def up + change_column :conversations, :cached_label_list, :text + end + + def down + # This might cause data loss if the text is longer than 255 characters + # lets start by truncating the data to 255 characters + Conversation.where('LENGTH(cached_label_list) > 255').find_in_batches do |conversation_batch| + Conversation.transaction do + conversation_batch.each do |conversation| + conversation.update!(cached_label_list: truncate_list(conversation.cached_label_list)) + end + end + end + + change_column :conversations, :cached_label_list, :string + end + + private + + # Truncate the list to 255 characters or less + # by removing the last element until the length is less than 255 + def truncate_list(label_list) + labels = label_list.split(',') + + # we add the `labels.length - 1` to account for the commas + labels.pop while (labels.join(',').length + labels.length - 1) > 255 + + labels.join(',') + end +end diff --git a/db/schema.rb b/db/schema.rb index 498ad13d3..a877bf05d 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: 2024_03_19_062553) do +ActiveRecord::Schema[7.0].define(version: 2024_03_22_071629) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" @@ -472,7 +472,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_03_19_062553) do t.integer "priority" t.bigint "sla_policy_id" t.datetime "waiting_since" - t.string "cached_label_list" + t.text "cached_label_list" t.index ["account_id", "display_id"], name: "index_conversations_on_account_id_and_display_id", unique: true t.index ["account_id", "id"], name: "index_conversations_on_id_and_account_id" t.index ["account_id", "inbox_id", "status", "assignee_id"], name: "conv_acid_inbid_stat_asgnid_idx" From d1dd319091f0c5e323049228e597d9f51c29e783 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 26 Mar 2024 09:22:49 +0530 Subject: [PATCH 2/7] feat: API to download breached conversations (#9150) * feat: add download conversations endpoint * feat: template for conversation list download * feat: setup download API and tests * chore: revert formatting change * feat: rename download method * feat: rename template * feat: include sla_policy table in download query * refactor: add nil safety to assignee * chore: Update en.yml * fix: remove applied_sla relation --- config/locales/en.yml | 9 ++++++ config/routes.rb | 1 + .../v1/accounts/applied_slas_controller.rb | 16 +++++++++- .../v1/accounts/applied_slas/download.csv.erb | 26 ++++++++++++++++ .../accounts/applied_slas_controller_spec.rb | 30 +++++++++++++++++++ 5 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 enterprise/app/views/api/v1/accounts/applied_slas/download.csv.erb diff --git a/config/locales/en.yml b/config/locales/en.yml index 4c4f0483c..92c56574d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -106,6 +106,15 @@ en: avg_resolution_time: Avg resolution time conversation_traffic_csv: timezone: Timezone + sla_csv: + conversation_id: Conversation ID + sla_policy_breached: SLA Policy + assignee: Assignee + team: Team + inbox: Inbox + labels: Labels + conversation_link: Link to the Conversation + breached_events: Breached Events default_group_by: day csat: headers: diff --git a/config/routes.rb b/config/routes.rb index 6be458e07..d002bd142 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -147,6 +147,7 @@ Rails.application.routes.draw do resources :applied_slas, only: [:index] do collection do get :metrics + get :download end end resources :custom_attribute_definitions, only: [:index, :show, :create, :update, :destroy] diff --git a/enterprise/app/controllers/api/v1/accounts/applied_slas_controller.rb b/enterprise/app/controllers/api/v1/accounts/applied_slas_controller.rb index 5eaaf2d68..4ec27bbbb 100644 --- a/enterprise/app/controllers/api/v1/accounts/applied_slas_controller.rb +++ b/enterprise/app/controllers/api/v1/accounts/applied_slas_controller.rb @@ -4,7 +4,7 @@ class Api::V1::Accounts::AppliedSlasController < Api::V1::Accounts::EnterpriseAc RESULTS_PER_PAGE = 25 - before_action :set_applied_slas, only: [:index, :metrics] + before_action :set_applied_slas, only: [:index, :metrics, :download] before_action :set_current_page, only: [:index] before_action :paginate_slas, only: [:index] before_action :check_admin_authorization? @@ -19,8 +19,22 @@ class Api::V1::Accounts::AppliedSlasController < Api::V1::Accounts::EnterpriseAc @hit_rate = hit_rate end + def download + @breached_slas = breached_slas + + response.headers['Content-Type'] = 'text/csv' + response.headers['Content-Disposition'] = 'attachment; filename=breached_conversation.csv' + render layout: false, formats: [:csv] + end + private + def breached_slas + @applied_slas.includes(:sla_policy).joins(:conversation) + .where.not(conversations: { status: :resolved }) + .where(applied_slas: { sla_status: :missed }) + end + def total_applied_slas @total_applied_slas ||= @applied_slas.count end diff --git a/enterprise/app/views/api/v1/accounts/applied_slas/download.csv.erb b/enterprise/app/views/api/v1/accounts/applied_slas/download.csv.erb new file mode 100644 index 000000000..676d6d680 --- /dev/null +++ b/enterprise/app/views/api/v1/accounts/applied_slas/download.csv.erb @@ -0,0 +1,26 @@ +<% headers = [ + I18n.t('reports.sla_csv.conversation_id'), + I18n.t('reports.sla_csv.sla_policy_breached'), + I18n.t('reports.sla_csv.assignee'), + I18n.t('reports.sla_csv.team'), + I18n.t('reports.sla_csv.inbox'), + I18n.t('reports.sla_csv.labels'), + I18n.t('reports.sla_csv.conversation_link'), + I18n.t('reports.sla_csv.breached_events') +] %> +<%= CSV.generate_line headers %> + +<% @breached_slas.each do |sla| %> + <% breached_events = sla.sla_events.map(&:event_type).join(', ') %> + <% conversation = sla.conversation %> + <%= CSV.generate_line([ + conversation.display_id, + sla.sla_policy.name, + conversation.assignee&.name, + conversation.team&.name, + conversation.inbox&.name, + conversation.cached_label_list, + app_account_conversation_url(account_id: conversation.account_id, id: conversation.display_id), + breached_events + ]) %> +<% end %> diff --git a/spec/enterprise/controllers/api/v1/accounts/applied_slas_controller_spec.rb b/spec/enterprise/controllers/api/v1/accounts/applied_slas_controller_spec.rb index e10d08f85..3945c8c93 100644 --- a/spec/enterprise/controllers/api/v1/accounts/applied_slas_controller_spec.rb +++ b/spec/enterprise/controllers/api/v1/accounts/applied_slas_controller_spec.rb @@ -104,6 +104,36 @@ RSpec.describe 'Applied SLAs API', type: :request do end end + describe 'GET /api/v1/accounts/{account.id}/applied_slas/download' do + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + get "/api/v1/accounts/#{account.id}/applied_slas/download" + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + it 'returns a CSV file with breached conversations' do + create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, sla_status: 'missed') + create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, sla_status: 'missed') + conversation1.update(status: 'open') + conversation2.update(status: 'resolved') + + get "/api/v1/accounts/#{account.id}/applied_slas/download", + headers: administrator.create_new_auth_token + + expect(response).to have_http_status(:success) + expect(response.headers['Content-Type']).to eq('text/csv') + expect(response.headers['Content-Disposition']).to include('attachment; filename=breached_conversation.csv') + + csv_data = CSV.parse(response.body) + csv_data.reject! { |row| row.all?(&:nil?) } + expect(csv_data.size).to eq(2) + expect(csv_data[1][0].to_i).to eq(conversation1.display_id) + end + end + end + describe 'GET /api/v1/accounts/{account.id}/applied_slas' do context 'when it is an unauthenticated user' do it 'returns unauthorized' do From 3b7694b163c37d0343ebe16e7107102f85bb170b Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Wed, 27 Mar 2024 03:42:09 +0530 Subject: [PATCH 3/7] chore(snyk): Security upgrade markdown-it from 13.0.1 to 13.0.2 (#9153) fix: package.json & yarn.lock to reduce vulnerabilities The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JS-MARKDOWNIT-6483324 Co-authored-by: snyk-bot --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index fcf7e23fa..313821424 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "libphonenumber-js": "^1.10.24", "logrocket": "^3.0.1", "logrocket-vuex": "^0.0.3", - "markdown-it": "^13.0.1", + "markdown-it": "^13.0.2", "markdown-it-link-attributes": "^4.0.1", "md5": "^2.3.0", "ninja-keys": "^1.2.2", diff --git a/yarn.lock b/yarn.lock index 9e919e2f4..80d048fa2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14551,10 +14551,10 @@ markdown-it@^10.0.0: mdurl "^1.0.1" uc.micro "^1.0.5" -markdown-it@^13.0.1: - version "13.0.1" - resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-13.0.1.tgz#c6ecc431cacf1a5da531423fc6a42807814af430" - integrity sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q== +markdown-it@^13.0.2: + version "13.0.2" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-13.0.2.tgz#1bc22e23379a6952e5d56217fbed881e0c94d536" + integrity sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w== dependencies: argparse "^2.0.1" entities "~3.0.1" From cdcf02c94340c92322c6f4c18e279ee362982698 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Mar 2024 15:12:39 -0700 Subject: [PATCH 4/7] chore(deps): bump follow-redirects from 1.15.3 to 1.15.6 (#9119) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.3 to 1.15.6. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.3...v1.15.6) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 80d048fa2..c23d9fb70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11407,9 +11407,9 @@ flush-write-stream@^1.0.0: readable-stream "^2.3.6" follow-redirects@^1.0.0, follow-redirects@^1.15.0: - version "1.15.3" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" - integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== for-each@^0.3.3: version "0.3.3" From 2ee911e33a86c05ab1fb115a99487f36074e05e3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Mar 2024 15:22:06 -0700 Subject: [PATCH 5/7] chore(deps): bump express from 4.18.2 to 4.19.2 (#9159) Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2. - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/master/History.md) - [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2) --- updated-dependencies: - dependency-name: express dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/yarn.lock b/yarn.lock index c23d9fb70..71040ce17 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8140,13 +8140,13 @@ bn.js@^5.2.1: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== -body-parser@1.20.1: - version "1.20.1" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" - integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== +body-parser@1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== dependencies: bytes "3.1.2" - content-type "~1.0.4" + content-type "~1.0.5" debug "2.6.9" depd "2.0.0" destroy "1.2.0" @@ -8154,7 +8154,7 @@ body-parser@1.20.1: iconv-lite "0.4.24" on-finished "2.4.1" qs "6.11.0" - raw-body "2.5.1" + raw-body "2.5.2" type-is "~1.6.18" unpipe "1.0.0" @@ -9129,6 +9129,11 @@ content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== +content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" @@ -9146,10 +9151,10 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== -cookie@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" - integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== copy-concurrently@^1.0.0: version "1.0.5" @@ -11054,16 +11059,16 @@ expect@^29.0.0, expect@^29.7.0: jest-util "^29.7.0" express@^4.17.1: - version "4.18.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" - integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + version "4.19.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" + integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== dependencies: accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.20.1" + body-parser "1.20.2" content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.5.0" + cookie "0.6.0" cookie-signature "1.0.6" debug "2.6.9" depd "2.0.0" @@ -17578,10 +17583,10 @@ range-parser@^1.2.1, range-parser@~1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" - integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== dependencies: bytes "3.1.2" http-errors "2.0.0" From 125326438273c2092b6806c6faa9b68fa7712f51 Mon Sep 17 00:00:00 2001 From: Shivam Kumar <76581658+shivamkb17@users.noreply.github.com> Date: Wed, 27 Mar 2024 07:28:45 +0530 Subject: [PATCH 6/7] fix: Avoid duplicate invitation emails when adding an agent (#9131) Co-authored-by: Sojan Co-authored-by: Shivam Mishra Co-authored-by: Pranav --- app/builders/agent_builder.rb | 6 ------ spec/builders/agent_builder_spec.rb | 16 ---------------- 2 files changed, 22 deletions(-) diff --git a/app/builders/agent_builder.rb b/app/builders/agent_builder.rb index 07b0e7345..54f478920 100644 --- a/app/builders/agent_builder.rb +++ b/app/builders/agent_builder.rb @@ -16,7 +16,6 @@ class AgentBuilder def perform ActiveRecord::Base.transaction do @user = find_or_create_user - send_confirmation_if_required create_account_user end @user @@ -34,11 +33,6 @@ class AgentBuilder User.create!(email: email, name: name, password: temp_password, password_confirmation: temp_password) end - # Sends confirmation instructions if the user is persisted and not confirmed. - def send_confirmation_if_required - @user.send_confirmation_instructions if user_needs_confirmation? - end - # Checks if the user needs confirmation. # @return [Boolean] true if the user is persisted and not confirmed, false otherwise. def user_needs_confirmation? diff --git a/spec/builders/agent_builder_spec.rb b/spec/builders/agent_builder_spec.rb index 9d7667306..ac8a3229a 100644 --- a/spec/builders/agent_builder_spec.rb +++ b/spec/builders/agent_builder_spec.rb @@ -67,21 +67,5 @@ RSpec.describe AgentBuilder, type: :model do expect(user.encrypted_password).not_to be_empty end end - - context 'with confirmation required' do - let(:unconfirmed_user) { create(:user, email: email) } - - before do - unconfirmed_user.confirmed_at = nil - unconfirmed_user.save(validate: false) - allow(unconfirmed_user).to receive(:confirmed?).and_return(false) - end - - it 'sends confirmation instructions' do - user = agent_builder.perform - expect(user).to receive(:send_confirmation_instructions) - agent_builder.send(:send_confirmation_if_required) - end - end end end From 3e07320d226acc07573c5e2a94d4e1193912698f Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Wed, 27 Mar 2024 13:19:51 +0530 Subject: [PATCH 7/7] feat: SLA threshold card component (#9163) - Component to display SLA timer in the conversation card and header --- .../conversation/components/SLACardLabel.vue | 103 ++++++++++++++++++ .../dashboard/helper/directives/resize.js | 41 +++++++ .../helper/specs/directives/resize.spec.js | 78 +++++++++++++ .../i18n/locale/en/conversation.json | 9 +- app/javascript/packs/application.js | 2 + 5 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 app/javascript/dashboard/components/widgets/conversation/components/SLACardLabel.vue create mode 100644 app/javascript/dashboard/helper/directives/resize.js create mode 100644 app/javascript/dashboard/helper/specs/directives/resize.spec.js diff --git a/app/javascript/dashboard/components/widgets/conversation/components/SLACardLabel.vue b/app/javascript/dashboard/components/widgets/conversation/components/SLACardLabel.vue new file mode 100644 index 000000000..381ca53b1 --- /dev/null +++ b/app/javascript/dashboard/components/widgets/conversation/components/SLACardLabel.vue @@ -0,0 +1,103 @@ + + + diff --git a/app/javascript/dashboard/helper/directives/resize.js b/app/javascript/dashboard/helper/directives/resize.js new file mode 100644 index 000000000..35e5315b0 --- /dev/null +++ b/app/javascript/dashboard/helper/directives/resize.js @@ -0,0 +1,41 @@ +import { debounce } from '@chatwoot/utils'; + +const RESIZE_OBSERVER_DEBOUNCE_TIME = 100; + +function createResizeObserver(el, binding) { + const { value } = binding; + const observer = new ResizeObserver( + debounce(entries => { + const entry = entries[0]; + if (entry && value && typeof value === 'function') { + value(entry); + } + }, RESIZE_OBSERVER_DEBOUNCE_TIME) + ); + + el.cwResizeObserver = observer; + observer.observe(el); +} + +function destroyResizeObserver(el) { + if (el.cwResizeObserver) { + el.cwResizeObserver.unobserve(el); + el.cwResizeObserver.disconnect(); + delete el.cwResizeObserver; + } +} + +export default { + bind(el, binding) { + createResizeObserver(el, binding); + }, + update(el, binding) { + if (binding.oldValue !== binding.value) { + destroyResizeObserver(el); + createResizeObserver(el, binding); + } + }, + unbind(el) { + destroyResizeObserver(el); + }, +}; diff --git a/app/javascript/dashboard/helper/specs/directives/resize.spec.js b/app/javascript/dashboard/helper/specs/directives/resize.spec.js new file mode 100644 index 000000000..fa099f40a --- /dev/null +++ b/app/javascript/dashboard/helper/specs/directives/resize.spec.js @@ -0,0 +1,78 @@ +import resize from '../../directives/resize'; + +class ResizeObserverMock { + // eslint-disable-next-line class-methods-use-this + observe() {} + + // eslint-disable-next-line class-methods-use-this + unobserve() {} + + // eslint-disable-next-line class-methods-use-this + disconnect() {} +} + +describe('resize directive', () => { + let el; + let binding; + let observer; + + beforeEach(() => { + el = document.createElement('div'); + binding = { + value: jest.fn(), + }; + observer = { + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), + }; + window.ResizeObserver = ResizeObserverMock; + jest.spyOn(window, 'ResizeObserver').mockImplementation(() => observer); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should create ResizeObserver on bind', () => { + resize.bind(el, binding); + + expect(ResizeObserver).toHaveBeenCalled(); + expect(observer.observe).toHaveBeenCalledWith(el); + }); + + it('should call callback on observer callback', () => { + el = document.createElement('div'); + binding = { + value: jest.fn(), + }; + + resize.bind(el, binding); + + const entries = [{ contentRect: { width: 100, height: 100 } }]; + const callback = binding.value; + callback(entries[0]); + + expect(binding.value).toHaveBeenCalledWith(entries[0]); + }); + + it('should destroy and recreate observer on update', () => { + resize.bind(el, binding); + + resize.update(el, { ...binding, oldValue: 'old' }); + + expect(observer.unobserve).toHaveBeenCalledWith(el); + expect(observer.disconnect).toHaveBeenCalled(); + expect(ResizeObserver).toHaveBeenCalledTimes(2); + expect(observer.observe).toHaveBeenCalledTimes(2); + }); + + it('should destroy observer on unbind', () => { + resize.bind(el, binding); + + resize.unbind(el); + + expect(observer.unobserve).toHaveBeenCalledWith(el); + expect(observer.disconnect).toHaveBeenCalled(); + }); +}); diff --git a/app/javascript/dashboard/i18n/locale/en/conversation.json b/app/javascript/dashboard/i18n/locale/en/conversation.json index 227c802d6..2bdf2af7a 100644 --- a/app/javascript/dashboard/i18n/locale/en/conversation.json +++ b/app/javascript/dashboard/i18n/locale/en/conversation.json @@ -64,7 +64,14 @@ "SNOOZED_UNTIL": "Snoozed until", "SNOOZED_UNTIL_TOMORROW": "Snoozed until tomorrow", "SNOOZED_UNTIL_NEXT_WEEK": "Snoozed until next week", - "SNOOZED_UNTIL_NEXT_REPLY": "Snoozed until next reply" + "SNOOZED_UNTIL_NEXT_REPLY": "Snoozed until next reply", + "SLA_STATUS": { + "FRT": "FRT {status}", + "NRT": "NRT {status}", + "RT": "RT {status}", + "BREACH": "breach", + "DUE": "due" + } }, "RESOLVE_DROPDOWN": { "MARK_PENDING": "Mark as pending", diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 18354bc62..61d6dec8d 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -30,6 +30,7 @@ import FluentIcon from 'shared/components/FluentIcon/DashboardIcon'; import VueDOMPurifyHTML from 'vue-dompurify-html'; import { domPurifyConfig } from '../shared/helpers/HTMLSanitizer'; import AnalyticsPlugin from '../dashboard/helper/AnalyticsHelper/plugin'; +import resizeDirective from '../dashboard/helper/directives/resize.js'; Vue.config.env = process.env; @@ -78,6 +79,7 @@ Vue.component('woot-switch', WootSwitch); Vue.component('woot-wizard', WootWizard); Vue.component('fluent-icon', FluentIcon); +Vue.directive('resize', resizeDirective); const i18nConfig = new VueI18n({ locale: 'en', messages: i18n,