From 7320957405c21b1f428e8d36910ed65852c4d64b Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Tue, 20 Feb 2024 15:53:29 +0530 Subject: [PATCH 01/65] chore: Add facebook_api_version to global config (#8965) - Ability to configure facebook_api_version from global config Co-authored-by: Pranav --- app/controllers/dashboard_controller.rb | 2 +- app/controllers/super_admin/app_configs_controller.rb | 2 +- config/installation_config.yml | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 9e59758ea..0aea9df83 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -55,7 +55,7 @@ class DashboardController < ActionController::Base VAPID_PUBLIC_KEY: VapidService.public_key, ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'), FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''), - FACEBOOK_API_VERSION: 'v14.0', + FACEBOOK_API_VERSION: GlobalConfigService.load('FACEBOOK_API_VERSION', 'v17.0'), IS_ENTERPRISE: ChatwootApp.enterprise?, AZURE_APP_ID: ENV.fetch('AZURE_APP_ID', ''), GIT_SHA: GIT_HASH diff --git a/app/controllers/super_admin/app_configs_controller.rb b/app/controllers/super_admin/app_configs_controller.rb index a31d01675..6223f7174 100644 --- a/app/controllers/super_admin/app_configs_controller.rb +++ b/app/controllers/super_admin/app_configs_controller.rb @@ -34,7 +34,7 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController def allowed_configs @allowed_configs = case @config when 'facebook' - %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET IG_VERIFY_TOKEN ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT] + %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET IG_VERIFY_TOKEN FACEBOOK_API_VERSION ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT] when 'email' ['MAILER_INBOUND_EMAIL_DOMAIN'] else diff --git a/config/installation_config.yml b/config/installation_config.yml index 60eae3bc2..9db527832 100644 --- a/config/installation_config.yml +++ b/config/installation_config.yml @@ -110,6 +110,11 @@ display_title: 'Instagram Verify Token' description: 'The verify token used for Instagram Webhook' locked: false +- name: FACEBOOK_API_VERSION + display_title: 'Facebook API Version' + description: 'Configure this if you want to use a different Facebook API version. Make sure its prefixed with `v`' + value: 'v17.0' + locked: false - name: ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT display_title: 'Enable human agent' value: false From 721a2f50525eeb116db4f3b8b96ac3588fb0ac56 Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Tue, 20 Feb 2024 03:18:51 -0800 Subject: [PATCH 02/65] feat: API changes to support multi step user signup (#8933) -API Changes to support the new onboarding flow Co-authored-by: Sojan --- .../api/v1/accounts/agents_controller.rb | 5 +++ app/controllers/api/v1/accounts_controller.rb | 1 + app/controllers/api/v2/accounts_controller.rb | 13 +++++++ .../api/v1/models/_account.json.jbuilder | 5 +++ .../enterprise/api/v2/accounts_controller.rb | 35 +++++++++++-------- .../api/v1/accounts_controller_spec.rb | 19 ++++++++++ .../api/v2/accounts_controller_spec.rb | 14 ++++++++ .../api/v1/accounts/agents_controller_spec.rb | 10 ++++++ .../api/v2/accounts_controller_spec.rb | 16 +++++++++ 9 files changed, 103 insertions(+), 15 deletions(-) diff --git a/app/controllers/api/v1/accounts/agents_controller.rb b/app/controllers/api/v1/accounts/agents_controller.rb index 221c96b85..eff9975f7 100644 --- a/app/controllers/api/v1/accounts/agents_controller.rb +++ b/app/controllers/api/v1/accounts/agents_controller.rb @@ -49,6 +49,11 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController Rails.logger.info "[Agent#bulk_create] ignoring email #{email}, errors: #{e.record.errors}" end end + + # This endpoint is used to bulk create agents during onboarding + # onboarding_step key in present in Current account custom attributes, since this is a one time operation + Current.account.custom_attributes.delete('onboarding_step') + Current.account.save! head :ok end diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 481594ee4..c0dac6d9e 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -46,6 +46,7 @@ class Api::V1::AccountsController < Api::BaseController def update @account.assign_attributes(account_params.slice(:name, :locale, :domain, :support_email, :auto_resolve_duration)) @account.custom_attributes.merge!(custom_attributes_params) + @account.custom_attributes['onboarding_step'] = 'invite_team' if @account.custom_attributes['onboarding_step'] == 'account_update' @account.save! end diff --git a/app/controllers/api/v2/accounts_controller.rb b/app/controllers/api/v2/accounts_controller.rb index ea19727ad..45faca3d0 100644 --- a/app/controllers/api/v2/accounts_controller.rb +++ b/app/controllers/api/v2/accounts_controller.rb @@ -22,6 +22,7 @@ class Api::V2::AccountsController < Api::BaseController ).perform fetch_account_and_user_info + update_account_info if @account.present? if @user send_auth_headers(@user) @@ -33,6 +34,18 @@ class Api::V2::AccountsController < Api::BaseController private + def account_attributes + { + custom_attributes: @account.custom_attributes.merge({ 'onboarding_step' => 'profile_update' }) + } + end + + def update_account_info + @account.update!( + account_attributes + ) + end + def fetch_account_and_user_info; end def fetch_account diff --git a/app/views/api/v1/models/_account.json.jbuilder b/app/views/api/v1/models/_account.json.jbuilder index 9f89a5510..5e9d9048a 100644 --- a/app/views/api/v1/models/_account.json.jbuilder +++ b/app/views/api/v1/models/_account.json.jbuilder @@ -6,6 +6,11 @@ if resource.custom_attributes.present? json.subscribed_quantity resource.custom_attributes['subscribed_quantity'] json.subscription_status resource.custom_attributes['subscription_status'] json.subscription_ends_on resource.custom_attributes['subscription_ends_on'] + json.industry resource.custom_attributes['industry'] if resource.custom_attributes['industry'].present? + json.company_size resource.custom_attributes['company_size'] if resource.custom_attributes['company_size'].present? + json.timezone resource.custom_attributes['timezone'] if resource.custom_attributes['timezone'].present? + json.logo resource.custom_attributes['logo'] if resource.custom_attributes['logo'].present? + json.onboarding_step resource.custom_attributes['onboarding_step'] if resource.custom_attributes['onboarding_step'].present? end end json.domain @account.domain diff --git a/enterprise/app/controllers/enterprise/api/v2/accounts_controller.rb b/enterprise/app/controllers/enterprise/api/v2/accounts_controller.rb index d0d76c1ae..3edefbb5f 100644 --- a/enterprise/app/controllers/enterprise/api/v2/accounts_controller.rb +++ b/enterprise/app/controllers/enterprise/api/v2/accounts_controller.rb @@ -2,12 +2,11 @@ module Enterprise::Api::V2::AccountsController private def fetch_account_and_user_info - data = fetch_from_clearbit + @data = fetch_from_clearbit - return if data.blank? + return if @data.blank? - update_user_info(data) - update_account_info(data) + update_user_info end def fetch_from_clearbit @@ -17,19 +16,25 @@ module Enterprise::Api::V2::AccountsController nil end - def update_user_info(data) - @user.update!(name: data[:name]) + def update_user_info + @user.update!(name: @data[:name]) if @data[:name].present? end - def update_account_info(data) - @account.update!( - name: data[:company_name], - custom_attributes: @account.custom_attributes.merge( - 'industry' => data[:industry], - 'company_size' => data[:company_size], - 'timezone' => data[:timezone], - 'logo' => data[:logo] - ) + def data_from_clearbit + return {} if @data.blank? + + { name: @data[:company_name], + custom_attributes: { + 'industry' => @data[:industry], + 'company_size' => @data[:company_size], + 'timezone' => @data[:timezone], + 'logo' => @data[:logo] + } } + end + + def account_attributes + super.deep_merge( + data_from_clearbit ) end end diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb index 95fa2ae7f..bd604f650 100644 --- a/spec/controllers/api/v1/accounts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts_controller_spec.rb @@ -213,6 +213,25 @@ RSpec.describe 'Accounts API', type: :request do end end + it 'updates onboarding step to invite_team if onboarding step is present in account custom attributes' do + account.update(custom_attributes: { onboarding_step: 'account_update' }) + put "/api/v1/accounts/#{account.id}", + params: params, + headers: admin.create_new_auth_token, + as: :json + + expect(account.reload.custom_attributes['onboarding_step']).to eq('invite_team') + end + + it 'will not update onboarding step if onboarding step is not present in account custom attributes' do + put "/api/v1/accounts/#{account.id}", + params: params, + headers: admin.create_new_auth_token, + as: :json + + expect(account.reload.custom_attributes['onboarding_step']).to be_nil + end + it 'Throws error 422' do params[:name] = 'test' * 999 diff --git a/spec/controllers/api/v2/accounts_controller_spec.rb b/spec/controllers/api/v2/accounts_controller_spec.rb index 82693ed2b..182ebadac 100644 --- a/spec/controllers/api/v2/accounts_controller_spec.rb +++ b/spec/controllers/api/v2/accounts_controller_spec.rb @@ -30,6 +30,20 @@ RSpec.describe 'Accounts API', type: :request do end end + it 'updates the onboarding step in custom attributes' do + with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do + allow(account_builder).to receive(:perform).and_return([user, account]) + + params = { email: email, user: nil, locale: nil, password: 'Password1!' } + + post api_v2_accounts_url, + params: params, + as: :json + + expect(account.reload.custom_attributes['onboarding_step']).to eq('profile_update') + end + end + it 'calls ChatwootCaptcha' do with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do captcha = double diff --git a/spec/enterprise/controllers/api/v1/accounts/agents_controller_spec.rb b/spec/enterprise/controllers/api/v1/accounts/agents_controller_spec.rb index 97e76a6cd..e5a8a5b7d 100644 --- a/spec/enterprise/controllers/api/v1/accounts/agents_controller_spec.rb +++ b/spec/enterprise/controllers/api/v1/accounts/agents_controller_spec.rb @@ -41,5 +41,15 @@ RSpec.describe 'Agents API', type: :request do expect(response.body).to include('Account limit exceeded. Please purchase more licenses') end end + + context 'when onboarding step is present in account custom attributes' do + it 'removes onboarding step from account custom attributes' do + account.update(custom_attributes: { onboarding_step: 'completed' }) + + post "/api/v1/accounts/#{account.id}/agents/bulk_create", params: bulk_create_params, headers: admin.create_new_auth_token + + expect(account.reload.custom_attributes).not_to include('onboarding_step') + end + end end end diff --git a/spec/enterprise/controllers/enterprise/api/v2/accounts_controller_spec.rb b/spec/enterprise/controllers/enterprise/api/v2/accounts_controller_spec.rb index 8ad75b42f..c74a2bcb5 100644 --- a/spec/enterprise/controllers/enterprise/api/v2/accounts_controller_spec.rb +++ b/spec/enterprise/controllers/enterprise/api/v2/accounts_controller_spec.rb @@ -51,6 +51,22 @@ RSpec.describe Enterprise::Api::V2::AccountsController, type: :request do end end + it 'updates the onboarding step in custom attributes' do + with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do + allow(account_builder).to receive(:perform).and_return([user, account]) + + params = { email: email, user: nil, locale: nil, password: 'Password1!' } + + post api_v2_accounts_url, + params: params, + as: :json + + custom_attributes = account.custom_attributes + + expect(custom_attributes['onboarding_step']).to eq('profile_update') + end + end + it 'handles errors when fetching data from clearbit' do with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do allow(account_builder).to receive(:perform).and_return([user, account]) From 5d9fb55370ccd9ea01043cbfe0e74f09569602e0 Mon Sep 17 00:00:00 2001 From: Liam <43280985+LiamAshdown@users.noreply.github.com> Date: Tue, 20 Feb 2024 11:41:03 +0000 Subject: [PATCH 03/65] feat: Export contact improvements (#8895) This pull request enhances the export contacts feature by adding a confirmation step before exporting. Previously, clicking the export button would trigger the export action without confirmation. Additionally, it ensures that only the intended recipient receives the export email, addressing the previous behaviour where all administrators received it. Fixes: #8504 Co-authored-by: Sojan Jose --- .../api/v1/accounts/contacts_controller.rb | 2 +- .../dashboard/i18n/locale/en/contact.json | 8 +++++++- .../dashboard/contacts/components/Header.vue | 16 ++++++++++++++-- app/jobs/account/contacts_export_job.rb | 4 ++-- .../channel_notifications_mailer.rb | 5 +++-- .../api/v1/accounts/contacts_controller_spec.rb | 4 ++-- spec/jobs/account/contacts_export_job_spec.rb | 6 +++--- .../channel_notifications_mailer_spec.rb | 5 ++--- 8 files changed, 34 insertions(+), 16 deletions(-) diff --git a/app/controllers/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb index f424f3b66..98683ea25 100644 --- a/app/controllers/api/v1/accounts/contacts_controller.rb +++ b/app/controllers/api/v1/accounts/contacts_controller.rb @@ -46,7 +46,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController def export column_names = params['column_names'] - Account::ContactsExportJob.perform_later(Current.account.id, column_names) + Account::ContactsExportJob.perform_later(Current.account.id, column_names, Current.user.email) head :ok, message: I18n.t('errors.contacts.export.success') end diff --git a/app/javascript/dashboard/i18n/locale/en/contact.json b/app/javascript/dashboard/i18n/locale/en/contact.json index 594e34c4f..cae908016 100644 --- a/app/javascript/dashboard/i18n/locale/en/contact.json +++ b/app/javascript/dashboard/i18n/locale/en/contact.json @@ -79,7 +79,13 @@ "TITLE": "Export Contacts", "DESC": "Export contacts to a CSV file.", "SUCCESS_MESSAGE": "Export is in progress. You will be notified on email when the export file is ready to download.", - "ERROR_MESSAGE": "There was an error, please try again" + "ERROR_MESSAGE": "There was an error, please try again", + "CONFIRM": { + "TITLE": "Export Contacts", + "MESSAGE": "Are you sure you want to export all contacts?", + "YES": "Yes, Export", + "NO": "No, Cancel" + } }, "DELETE_NOTE": { "CONFIRM": { diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/Header.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/Header.vue index 4df4017b2..1b4fd90e0 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/components/Header.vue +++ b/app/javascript/dashboard/routes/dashboard/contacts/components/Header.vue @@ -113,6 +113,13 @@ + @@ -175,8 +182,13 @@ export default { toggleImport() { this.$emit('on-toggle-import'); }, - submitExport() { - this.$emit('on-export-submit'); + async submitExport() { + const ok = + await this.$refs.confirmExportContactsDialog.showConfirmation(); + + if (ok) { + this.$emit('on-export-submit'); + } }, submitSearch() { this.$emit('on-search-submit'); diff --git a/app/jobs/account/contacts_export_job.rb b/app/jobs/account/contacts_export_job.rb index 0ae8b3892..b8e9dbc96 100644 --- a/app/jobs/account/contacts_export_job.rb +++ b/app/jobs/account/contacts_export_job.rb @@ -1,13 +1,13 @@ class Account::ContactsExportJob < ApplicationJob queue_as :low - def perform(account_id, column_names) + def perform(account_id, column_names, email_to) account = Account.find(account_id) headers = valid_headers(column_names) generate_csv(account, headers) file_url = account_contact_export_url(account) - AdministratorNotifications::ChannelNotificationsMailer.with(account: account).contact_export_complete(file_url)&.deliver_later + AdministratorNotifications::ChannelNotificationsMailer.with(account: account).contact_export_complete(file_url, email_to)&.deliver_later end def generate_csv(account, headers) diff --git a/app/mailers/administrator_notifications/channel_notifications_mailer.rb b/app/mailers/administrator_notifications/channel_notifications_mailer.rb index 8c52a9bd3..6c0f7cee2 100644 --- a/app/mailers/administrator_notifications/channel_notifications_mailer.rb +++ b/app/mailers/administrator_notifications/channel_notifications_mailer.rb @@ -60,12 +60,13 @@ class AdministratorNotifications::ChannelNotificationsMailer < ApplicationMailer send_mail_with_liquid(to: admin_emails, subject: subject) and return end - def contact_export_complete(file_url) + def contact_export_complete(file_url, email_to) return unless smtp_config_set_or_development? @action_url = file_url subject = "Your contact's export file is available to download." - send_mail_with_liquid(to: admin_emails, subject: subject) and return + + send_mail_with_liquid(to: email_to, subject: subject) and return end private diff --git a/spec/controllers/api/v1/accounts/contacts_controller_spec.rb b/spec/controllers/api/v1/accounts/contacts_controller_spec.rb index e5e298747..21511310b 100644 --- a/spec/controllers/api/v1/accounts/contacts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/contacts_controller_spec.rb @@ -197,7 +197,7 @@ RSpec.describe 'Contacts API', type: :request do let(:admin) { create(:user, account: account, role: :administrator) } it 'enqueues a contact export job' do - expect(Account::ContactsExportJob).to receive(:perform_later).with(account.id, nil).once + expect(Account::ContactsExportJob).to receive(:perform_later).with(account.id, nil, admin.email).once get "/api/v1/accounts/#{account.id}/contacts/export", headers: admin.create_new_auth_token, @@ -207,7 +207,7 @@ RSpec.describe 'Contacts API', type: :request do end it 'enqueues a contact export job with sent_columns' do - expect(Account::ContactsExportJob).to receive(:perform_later).with(account.id, %w[phone_number email]).once + expect(Account::ContactsExportJob).to receive(:perform_later).with(account.id, %w[phone_number email], admin.email).once get "/api/v1/accounts/#{account.id}/contacts/export", headers: admin.create_new_auth_token, diff --git a/spec/jobs/account/contacts_export_job_spec.rb b/spec/jobs/account/contacts_export_job_spec.rb index e597ff637..b395967b4 100644 --- a/spec/jobs/account/contacts_export_job_spec.rb +++ b/spec/jobs/account/contacts_export_job_spec.rb @@ -24,17 +24,17 @@ RSpec.describe Account::ContactsExportJob do allow(AdministratorNotifications::ChannelNotificationsMailer).to receive(:with).with(account: account).and_return(mailer) allow(mailer).to receive(:contact_export_complete) - described_class.perform_now(account.id, []) + described_class.perform_now(account.id, [], 'test@test.com') file_url = Rails.application.routes.url_helpers.rails_blob_url(account.contacts_export) expect(account.contacts_export).to be_present expect(file_url).to be_present - expect(mailer).to have_received(:contact_export_complete).with(file_url) + expect(mailer).to have_received(:contact_export_complete).with(file_url, 'test@test.com') end it 'generates valid data export file' do - described_class.perform_now(account.id, []) + described_class.perform_now(account.id, [], 'test@test.com') csv_data = CSV.parse(account.contacts_export.download, headers: true) emails = csv_data.pluck('email') diff --git a/spec/mailers/administrator_notifications/channel_notifications_mailer_spec.rb b/spec/mailers/administrator_notifications/channel_notifications_mailer_spec.rb index f67b8300c..944475fb2 100644 --- a/spec/mailers/administrator_notifications/channel_notifications_mailer_spec.rb +++ b/spec/mailers/administrator_notifications/channel_notifications_mailer_spec.rb @@ -10,7 +10,6 @@ RSpec.describe AdministratorNotifications::ChannelNotificationsMailer do before do allow(described_class).to receive(:new).and_return(class_instance) allow(class_instance).to receive(:smtp_config_set_or_development?).and_return(true) - Account::ContactsExportJob.perform_now(account.id, []) end describe 'slack_disconnect' do @@ -92,8 +91,8 @@ RSpec.describe AdministratorNotifications::ChannelNotificationsMailer do end describe 'contact_export_complete' do - let!(:file_url) { Rails.application.routes.url_helpers.rails_blob_url(account.contacts_export) } - let(:mail) { described_class.with(account: account).contact_export_complete(file_url).deliver_now } + let!(:file_url) { 'http://test.com/test' } + let(:mail) { described_class.with(account: account).contact_export_complete(file_url, administrator.email).deliver_now } it 'renders the subject' do expect(mail.subject).to eq("Your contact's export file is available to download.") From c5c08451517f86b3b95b5f836e80b1597ef23244 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Tue, 20 Feb 2024 17:17:25 +0530 Subject: [PATCH 04/65] feat: Add labels, status and priority in notification `push_event_data` (#8972) --- app/models/notification.rb | 10 +--------- .../api/v1/accounts/notifications_controller_spec.rb | 1 + 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/app/models/notification.rb b/app/models/notification.rb index f8c6e3d5b..cd3d57627 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -68,20 +68,12 @@ class Notification < ApplicationRecord } if primary_actor.present? - payload[:primary_actor] = primary_actor_data + payload[:primary_actor] = primary_actor&.push_event_data payload[:push_message_title] = push_message_title end payload end - def primary_actor_data - { - id: primary_actor.push_event_data[:id], - meta: primary_actor.push_event_data[:meta], - inbox_id: primary_actor.push_event_data[:inbox_id] - } - end - def fcm_push_data { id: id, diff --git a/spec/controllers/api/v1/accounts/notifications_controller_spec.rb b/spec/controllers/api/v1/accounts/notifications_controller_spec.rb index 3dcb47f71..4605e0fda 100644 --- a/spec/controllers/api/v1/accounts/notifications_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/notifications_controller_spec.rb @@ -29,6 +29,7 @@ RSpec.describe 'Notifications API', type: :request do expect(response_json['data']['meta']['count']).to eq 2 # notification appear in descending order expect(response_json['data']['payload'].first['id']).to eq notification2.id + expect(response_json['data']['payload'].first['primary_actor']).not_to be_nil end end end From e6cf8c39b7a3d4482c2a96dcc51995e910800255 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Tue, 20 Feb 2024 18:55:39 +0530 Subject: [PATCH 05/65] feat: Update `last_snoozed_at` after the un-snooze notification (#8943) --- .../v1/accounts/notifications_controller.rb | 3 ++- .../reopen_snoozed_notifications_job.rb | 23 +++++++++++++++---- app/models/notification.rb | 1 + .../notifications/index.json.jbuilder | 1 + .../accounts/notifications_controller_spec.rb | 1 + .../reopen_snoozed_notifications_job_spec.rb | 3 +++ 6 files changed, 27 insertions(+), 5 deletions(-) diff --git a/app/controllers/api/v1/accounts/notifications_controller.rb b/app/controllers/api/v1/accounts/notifications_controller.rb index 3ac0568e3..52035ce64 100644 --- a/app/controllers/api/v1/accounts/notifications_controller.rb +++ b/app/controllers/api/v1/accounts/notifications_controller.rb @@ -54,7 +54,8 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro end def snooze - @notification.update(snoozed_until: parse_date_time(params[:snoozed_until].to_s)) if params[:snoozed_until] + updated_meta = (@notification.meta || {}).merge('last_snoozed_at' => nil) + @notification.update(snoozed_until: parse_date_time(params[:snoozed_until].to_s), meta: updated_meta) if params[:snoozed_until] render json: @notification end diff --git a/app/jobs/notification/reopen_snoozed_notifications_job.rb b/app/jobs/notification/reopen_snoozed_notifications_job.rb index 05235b62b..91ed5ee61 100644 --- a/app/jobs/notification/reopen_snoozed_notifications_job.rb +++ b/app/jobs/notification/reopen_snoozed_notifications_job.rb @@ -2,9 +2,24 @@ class Notification::ReopenSnoozedNotificationsJob < ApplicationJob queue_as :low def perform - # rubocop:disable Rails/SkipsModelValidations - Notification.where(snoozed_until: 3.days.ago..Time.current) - .update_all(snoozed_until: nil, updated_at: Time.current, last_activity_at: Time.current, read_at: nil) - # rubocop:enable Rails/SkipsModelValidations + Notification.where(snoozed_until: 3.days.ago..Time.current).find_in_batches(batch_size: 100) do |notifications_batch| + notifications_batch.each do |notification| + update_notification(notification) + end + end + end + + private + + def update_notification(notification) + updated_meta = (notification.meta || {}).merge('last_snoozed_at' => notification.snoozed_until) + + notification.update!( + snoozed_until: nil, + updated_at: Time.current, + last_activity_at: Time.current, + meta: updated_meta, + read_at: nil + ) end end diff --git a/app/models/notification.rb b/app/models/notification.rb index cd3d57627..113c6cf3e 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -64,6 +64,7 @@ class Notification < ApplicationRecord created_at: created_at.to_i, last_activity_at: last_activity_at.to_i, snoozed_until: snoozed_until, + meta: meta, account_id: account_id } diff --git a/app/views/api/v1/accounts/notifications/index.json.jbuilder b/app/views/api/v1/accounts/notifications/index.json.jbuilder index ae86372c9..4a408dfca 100644 --- a/app/views/api/v1/accounts/notifications/index.json.jbuilder +++ b/app/views/api/v1/accounts/notifications/index.json.jbuilder @@ -21,6 +21,7 @@ json.data do json.created_at notification.created_at.to_i json.last_activity_at notification.last_activity_at.to_i json.snoozed_until notification.snoozed_until + json.meta notification.meta end end end diff --git a/spec/controllers/api/v1/accounts/notifications_controller_spec.rb b/spec/controllers/api/v1/accounts/notifications_controller_spec.rb index 4605e0fda..9b16e48fb 100644 --- a/spec/controllers/api/v1/accounts/notifications_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/notifications_controller_spec.rb @@ -179,6 +179,7 @@ RSpec.describe 'Notifications API', type: :request do expect(response).to have_http_status(:success) expect(notification.reload.snoozed_until).not_to eq('') + expect(notification.reload.meta['last_snoozed_at']).to be_nil end end end diff --git a/spec/jobs/notification/reopen_snoozed_notifications_job_spec.rb b/spec/jobs/notification/reopen_snoozed_notifications_job_spec.rb index 024c3d0d6..94697dcd6 100644 --- a/spec/jobs/notification/reopen_snoozed_notifications_job_spec.rb +++ b/spec/jobs/notification/reopen_snoozed_notifications_job_spec.rb @@ -14,10 +14,13 @@ RSpec.describe Notification::ReopenSnoozedNotificationsJob do it 'reopens snoozed notifications whose snooze until has passed' do described_class.perform_now + snoozed_until = snoozed_till_5_minutes_ago.reload.snoozed_until + expect(snoozed_till_5_minutes_ago.reload.snoozed_until).to be_nil expect(snoozed_till_tomorrow.reload.snoozed_until.to_date).to eq 1.day.from_now.to_date expect(snoozed_indefinitely.reload.snoozed_until).to be_nil expect(snoozed_indefinitely.reload.read_at).to be_nil + expect(snoozed_until).to eq(snoozed_till_5_minutes_ago.reload.meta['snoozed_until']) end end end From f92cea144cb7bf156593dfbe07f855a867223cef Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Tue, 20 Feb 2024 21:54:37 +0530 Subject: [PATCH 06/65] feat(perf): sla-9 improve perf of TriggerSlasForAccountsJob (#8953) * feat: improve perf of TriggerSlasForAccountsJob --- .../jobs/sla/trigger_slas_for_accounts_job.rb | 2 +- .../sla/trigger_slas_for_accounts_job_spec.rb | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/enterprise/app/jobs/sla/trigger_slas_for_accounts_job.rb b/enterprise/app/jobs/sla/trigger_slas_for_accounts_job.rb index 212c44d94..a2a142430 100644 --- a/enterprise/app/jobs/sla/trigger_slas_for_accounts_job.rb +++ b/enterprise/app/jobs/sla/trigger_slas_for_accounts_job.rb @@ -2,7 +2,7 @@ class Sla::TriggerSlasForAccountsJob < ApplicationJob queue_as :scheduled_jobs def perform - Account.find_each do |account| + Account.joins(:sla_policies).distinct.find_each do |account| Rails.logger.info "Enqueuing ProcessAccountAppliedSlasJob for account #{account.id}" Sla::ProcessAccountAppliedSlasJob.perform_later(account) end diff --git a/spec/enterprise/jobs/sla/trigger_slas_for_accounts_job_spec.rb b/spec/enterprise/jobs/sla/trigger_slas_for_accounts_job_spec.rb index 95650748a..d02e543ba 100644 --- a/spec/enterprise/jobs/sla/trigger_slas_for_accounts_job_spec.rb +++ b/spec/enterprise/jobs/sla/trigger_slas_for_accounts_job_spec.rb @@ -1,16 +1,25 @@ require 'rails_helper' - RSpec.describe Sla::TriggerSlasForAccountsJob do context 'when perform is called' do - let(:account) { create(:account) } + let(:account_with_sla) { create(:account) } + let(:account_without_sla) { create(:account) } + + before do + create(:sla_policy, account: account_with_sla) + end it 'enqueues the job' do expect { described_class.perform_later }.to have_enqueued_job(described_class) .on_queue('scheduled_jobs') end - it 'calls the ProcessAccountAppliedSlasJob' do - expect(Sla::ProcessAccountAppliedSlasJob).to receive(:perform_later).with(account).and_call_original + it 'calls the ProcessAccountAppliedSlasJob for accounts with SLA' do + expect(Sla::ProcessAccountAppliedSlasJob).to receive(:perform_later).with(account_with_sla).and_call_original + described_class.perform_now + end + + it 'does not call the ProcessAccountAppliedSlasJob for accounts without SLA' do + expect(Sla::ProcessAccountAppliedSlasJob).not_to receive(:perform_later).with(account_without_sla) described_class.perform_now end end From 23230e0143e70dc12c62e39af0bd78d874f998cf Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Tue, 20 Feb 2024 21:59:49 +0530 Subject: [PATCH 07/65] feat: sla-7 ensure applied_sla uniqueness (#8938) * feat: refactor fetching sla in action service * chore: modify spec * chore: ensure applied_sla uniqueness * chore: review fixes * feat: add unique index on applied_sla * chore: fix spec * chore: add new specs to improve coverage * chore: improve spec * Update spec/enterprise/services/enterprise/action_service_spec.rb --------- Co-authored-by: Sojan Jose --- app/services/action_service.rb | 1 + ...055809_add_unique_index_to_applied_slas.rb | 8 ++++ db/schema.rb | 3 +- enterprise/app/models/applied_sla.rb | 9 ++-- .../app/services/enterprise/action_service.rb | 10 ++++- .../enterprise/action_service_spec.rb | 44 +++++++++++++++---- 6 files changed, 61 insertions(+), 14 deletions(-) create mode 100644 db/migrate/20240216055809_add_unique_index_to_applied_slas.rb diff --git a/app/services/action_service.rb b/app/services/action_service.rb index 7a9a14d0d..79f1234a0 100644 --- a/app/services/action_service.rb +++ b/app/services/action_service.rb @@ -3,6 +3,7 @@ class ActionService def initialize(conversation) @conversation = conversation.reload + @account = @conversation.account end def mute_conversation(_params) diff --git a/db/migrate/20240216055809_add_unique_index_to_applied_slas.rb b/db/migrate/20240216055809_add_unique_index_to_applied_slas.rb new file mode 100644 index 000000000..1d64fc67d --- /dev/null +++ b/db/migrate/20240216055809_add_unique_index_to_applied_slas.rb @@ -0,0 +1,8 @@ +class AddUniqueIndexToAppliedSlas < ActiveRecord::Migration[7.0] + def change + add_index :applied_slas, + [:account_id, :sla_policy_id, :conversation_id], + unique: true, + name: 'index_applied_slas_on_account_sla_policy_conversation' + end +end diff --git a/db/schema.rb b/db/schema.rb index 138ea6835..546df444f 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_02_15_065844) do +ActiveRecord::Schema[7.0].define(version: 2024_02_16_055809) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" @@ -122,6 +122,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_02_15_065844) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "sla_status", default: 0 + t.index ["account_id", "sla_policy_id", "conversation_id"], name: "index_applied_slas_on_account_sla_policy_conversation", unique: true t.index ["account_id"], name: "index_applied_slas_on_account_id" t.index ["conversation_id"], name: "index_applied_slas_on_conversation_id" t.index ["sla_policy_id"], name: "index_applied_slas_on_sla_policy_id" diff --git a/enterprise/app/models/applied_sla.rb b/enterprise/app/models/applied_sla.rb index 825c747fc..cb5ca508d 100644 --- a/enterprise/app/models/applied_sla.rb +++ b/enterprise/app/models/applied_sla.rb @@ -12,14 +12,17 @@ # # Indexes # -# index_applied_slas_on_account_id (account_id) -# index_applied_slas_on_conversation_id (conversation_id) -# index_applied_slas_on_sla_policy_id (sla_policy_id) +# index_applied_slas_on_account_id (account_id) +# index_applied_slas_on_account_sla_policy_conversation (account_id,sla_policy_id,conversation_id) UNIQUE +# index_applied_slas_on_conversation_id (conversation_id) +# index_applied_slas_on_sla_policy_id (sla_policy_id) # class AppliedSla < ApplicationRecord belongs_to :account belongs_to :sla_policy belongs_to :conversation + validates :account_id, uniqueness: { scope: %i[sla_policy_id conversation_id] } + enum sla_status: { active: 0, hit: 1, missed: 2 } end diff --git a/enterprise/app/services/enterprise/action_service.rb b/enterprise/app/services/enterprise/action_service.rb index 6e21031aa..1e4165b09 100644 --- a/enterprise/app/services/enterprise/action_service.rb +++ b/enterprise/app/services/enterprise/action_service.rb @@ -1,10 +1,18 @@ module Enterprise::ActionService - def add_sla(sla_policy) + def add_sla(sla_policy_id) + return if sla_policy_id.blank? + + sla_policy = @account.sla_policies.find_by(id: sla_policy_id.first) + return if sla_policy.nil? + return if @conversation.sla_policy.present? + + Rails.logger.info "SLA:: Adding SLA #{sla_policy.id} to conversation: #{@conversation.id}" @conversation.update!(sla_policy_id: sla_policy.id) create_applied_sla(sla_policy) end def create_applied_sla(sla_policy) + Rails.logger.info "SLA:: Creating Applied SLA for conversation: #{@conversation.id}" AppliedSla.create!( account_id: @conversation.account_id, sla_policy_id: sla_policy.id, diff --git a/spec/enterprise/services/enterprise/action_service_spec.rb b/spec/enterprise/services/enterprise/action_service_spec.rb index 6efffca6d..a77a039dd 100644 --- a/spec/enterprise/services/enterprise/action_service_spec.rb +++ b/spec/enterprise/services/enterprise/action_service_spec.rb @@ -8,16 +8,42 @@ describe ActionService do let(:conversation) { create(:conversation, account: account) } let(:action_service) { described_class.new(conversation) } - it 'adds the sla policy to the conversation and create applied_sla entry' do - action_service.add_sla(sla_policy) - expect(conversation.reload.sla_policy_id).to eq(sla_policy.id) + context 'when sla_policy_id is present' do + it 'adds the sla policy to the conversation and create applied_sla entry' do + action_service.add_sla([sla_policy.id]) + expect(conversation.reload.sla_policy_id).to eq(sla_policy.id) - # check if appliedsla table entry is created with matching attributes - applied_sla = AppliedSla.last - expect(applied_sla.account_id).to eq(account.id) - expect(applied_sla.sla_policy_id).to eq(sla_policy.id) - expect(applied_sla.conversation_id).to eq(conversation.id) - expect(applied_sla.sla_status).to eq('active') + # check if appliedsla table entry is created with matching attributes + applied_sla = AppliedSla.last + expect(applied_sla.account_id).to eq(account.id) + expect(applied_sla.sla_policy_id).to eq(sla_policy.id) + expect(applied_sla.conversation_id).to eq(conversation.id) + expect(applied_sla.sla_status).to eq('active') + end + end + + context 'when sla_policy_id is not present' do + it 'does not add the sla policy to the conversation' do + action_service.add_sla(nil) + expect(conversation.reload.sla_policy_id).to be_nil + end + end + + context 'when conversation already has a sla policy' do + it 'does not add the new sla policy to the conversation' do + existing_sla_policy = sla_policy + new_sla_policy = create(:sla_policy, account: account) + conversation.update!(sla_policy_id: existing_sla_policy.id) + action_service.add_sla([new_sla_policy.id]) + expect(conversation.reload.sla_policy_id).to eq(existing_sla_policy.id) + end + end + + context 'when sla_policy is not found' do + it 'does not add the sla policy to the conversation' do + action_service.add_sla([sla_policy.id + 1]) + expect(conversation.reload.sla_policy_id).to be_nil + end end end end From 19aef3e94b395d4d140842130bdb2e74abe5e5c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Feb 2024 16:11:02 -0800 Subject: [PATCH 08/65] chore(deps): bump ip from 1.1.5 to 1.1.9 (#8976) Bumps [ip](https://github.com/indutny/node-ip) from 1.1.5 to 1.1.9. - [Commits](https://github.com/indutny/node-ip/compare/v1.1.5...v1.1.9) --- updated-dependencies: - dependency-name: ip dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index 1ab16149d..cc9a6f34c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12597,14 +12597,14 @@ ip-regex@^2.1.0: integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= ip@^1.1.0, ip@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" - integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= + version "1.1.9" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.9.tgz#8dfbcc99a754d07f425310b86a99546b1151e396" + integrity sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ== ip@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" - integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.1.tgz#e8f3595d33a3ea66490204234b77636965307105" + integrity sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ== ipaddr.js@1.9.1, ipaddr.js@^1.9.0: version "1.9.1" From 9911c5dc12495beb7a3c838c89e43fa7adc3f737 Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Tue, 20 Feb 2024 16:12:51 -0800 Subject: [PATCH 09/65] chore: Hide banners on onboarding view (#8934) --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> --- app/javascript/dashboard/App.vue | 8 +++++-- app/javascript/v3/helpers/RouteHelper.js | 10 ++++++++ .../v3/helpers/specs/RouteHelper.spec.js | 23 ++++++++++++++++++- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/app/javascript/dashboard/App.vue b/app/javascript/dashboard/App.vue index 49fe325f6..481c671ce 100644 --- a/app/javascript/dashboard/App.vue +++ b/app/javascript/dashboard/App.vue @@ -8,8 +8,8 @@ > @@ -38,6 +38,7 @@ import vueActionCable from './helper/actionCable'; import WootSnackbarBox from './components/SnackbarContainer.vue'; import rtlMixin from 'shared/mixins/rtlMixin'; import { setColorTheme } from './helper/themeHelper'; +import { isOnOnboardingView } from 'v3/helpers/RouteHelper'; import { registerSubscription, verifyServiceWorkerExistence, @@ -79,6 +80,9 @@ export default { const { accounts = [] } = this.currentUser || {}; return accounts.length > 0; }, + hideOnOnboardingView() { + return !isOnOnboardingView(this.$route); + }, }, watch: { diff --git a/app/javascript/v3/helpers/RouteHelper.js b/app/javascript/v3/helpers/RouteHelper.js index db508ac13..16bc9cd41 100644 --- a/app/javascript/v3/helpers/RouteHelper.js +++ b/app/javascript/v3/helpers/RouteHelper.js @@ -48,3 +48,13 @@ export const validateRouteAccess = (to, next, chatwootConfig = {}) => { next(); }; + +export const isOnOnboardingView = route => { + const { name = '' } = route || {}; + + if (!name) { + return false; + } + + return name.includes('onboarding_'); +}; diff --git a/app/javascript/v3/helpers/specs/RouteHelper.spec.js b/app/javascript/v3/helpers/specs/RouteHelper.spec.js index 8481a8ff4..4ebeccdcb 100644 --- a/app/javascript/v3/helpers/specs/RouteHelper.spec.js +++ b/app/javascript/v3/helpers/specs/RouteHelper.spec.js @@ -1,4 +1,4 @@ -import { validateRouteAccess } from '../RouteHelper'; +import { validateRouteAccess, isOnOnboardingView } from '../RouteHelper'; import { clearBrowserSessionCookies } from 'dashboard/store/utils/api'; import { replaceRouteWithReload } from '../CommonHelper'; import Cookies from 'js-cookie'; @@ -67,3 +67,24 @@ describe('#validateRouteAccess', () => { expect(next).toHaveBeenCalledWith(); }); }); + +describe('isOnOnboardingView', () => { + test('returns true for a route with onboarding name', () => { + const route = { name: 'onboarding_welcome' }; + expect(isOnOnboardingView(route)).toBe(true); + }); + + test('returns false for a route without onboarding name', () => { + const route = { name: 'home' }; + expect(isOnOnboardingView(route)).toBe(false); + }); + + test('returns false for a route with null name', () => { + const route = { name: null }; + expect(isOnOnboardingView(route)).toBe(false); + }); + + test('returns false for an undefined route object', () => { + expect(isOnOnboardingView()).toBe(false); + }); +}); From d53097f77d2af04e4380231682f082d89ca26bc9 Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Wed, 21 Feb 2024 06:33:39 +0530 Subject: [PATCH 10/65] fix: Raise error if email to_header is invalid (#8688) --- app/mailboxes/application_mailbox.rb | 14 +++++++++ spec/fixtures/files/mail_with_invalid_to.eml | 29 +++++++++++++++++++ .../fixtures/files/mail_with_invalid_to_2.eml | 29 +++++++++++++++++++ spec/mailboxes/application_mailbox_spec.rb | 18 ++++++++++++ 4 files changed, 90 insertions(+) create mode 100644 spec/fixtures/files/mail_with_invalid_to.eml create mode 100644 spec/fixtures/files/mail_with_invalid_to_2.eml diff --git a/app/mailboxes/application_mailbox.rb b/app/mailboxes/application_mailbox.rb index c1e273e70..6a1902ce4 100644 --- a/app/mailboxes/application_mailbox.rb +++ b/app/mailboxes/application_mailbox.rb @@ -37,10 +37,24 @@ class ApplicationMailbox < ActionMailbox::Base # checks if follow this pattern send it to reply_mailbox # reply+@ def reply_uuid_mail?(inbound_mail) + validate_to_address(inbound_mail) + inbound_mail.mail.to&.any? do |email| conversation_uuid = email.split('@')[0] conversation_uuid.match?(REPLY_EMAIL_UUID_PATTERN) end end + + # if mail.to returns a string, then it is a malformed `to` header + # valid `to` header will be of type Mail::AddressContainer + # validate if the to address is of type string + def validate_to_address(inbound_mail) + to_address_class = inbound_mail.mail.to&.class + + return if to_address_class == Mail::AddressContainer + + Rails.logger.error "Email to address header is malformed `#{inbound_mail.mail.to}`" + raise StandardError, "Invalid email to address header #{inbound_mail.mail.to}" + end end end diff --git a/spec/fixtures/files/mail_with_invalid_to.eml b/spec/fixtures/files/mail_with_invalid_to.eml new file mode 100644 index 000000000..8dd2aabb6 --- /dev/null +++ b/spec/fixtures/files/mail_with_invalid_to.eml @@ -0,0 +1,29 @@ +X-Original-To: bd84c730a1ac7833e4d27253804516f7@reply.chatwoot.com +Received: from mail.planetmars.com (mxd [192.168.1.1]) by mx.sendgrid.net with ESMTP id AAAA-bCCCCCC5DeeeFFgg for ; Sun, 31 Dec 2023 22:32:23.586 +0000 (UTC) +From: "Mark Whatney" +To: vishnu@chatwoot.com +Subject: stranded in mars +Date: Mon, 1 Jan 2024 06:31:44 +0800 +Message-ID: <1234560e0123c05b4bbf83c828b1688a93c7@com> +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary=15688136a4ad411d82b004fae6e46549 +X-Exim-Id: 1234560e0123c05b4bbf83c828b1688a93c7 + +This is a multipart message in MIME format. + +--15688136a4ad411d82b004fae6e46549 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: 7bit + +hey v, can i get some help over here? + +--15688136a4ad411d82b004fae6e46549 +Content-Type: text/html; + charset="utf-8" +Content-Transfer-Encoding: quoted-printable + +hey v, can i get some help over here? + +--15688136a4ad411d82b004fae6e46549-- diff --git a/spec/fixtures/files/mail_with_invalid_to_2.eml b/spec/fixtures/files/mail_with_invalid_to_2.eml new file mode 100644 index 000000000..4ef77cb2d --- /dev/null +++ b/spec/fixtures/files/mail_with_invalid_to_2.eml @@ -0,0 +1,29 @@ +X-Original-To: bd84c730a1ac7833e4d27253804516f7@reply.chatwoot.com +Received: from mail.planetmars.com (mxd [192.168.1.1]) by mx.sendgrid.net with ESMTP id AAAA-bCCCCCC5DeeeFFgg for ; Sun, 31 Dec 2023 22:32:23.586 +0000 (UTC) +From: "Mark Whatney" +To: vishnu@chatwoot.com www.chatwoot.com +Subject: stranded in mars +Date: Mon, 1 Jan 2024 06:31:44 +0800 +Message-ID: <1234560e0123c05b4bbf83c828b1688a93c7@com> +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary=15688136a4ad411d82b004fae6e46549 +X-Exim-Id: 1234560e0123c05b4bbf83c828b1688a93c7 + +This is a multipart message in MIME format. + +--15688136a4ad411d82b004fae6e46549 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: 7bit + +hey v, can i get some help over here? + +--15688136a4ad411d82b004fae6e46549 +Content-Type: text/html; + charset="utf-8" +Content-Transfer-Encoding: quoted-printable + +hey v, can i get some help over here? + +--15688136a4ad411d82b004fae6e46549-- diff --git a/spec/mailboxes/application_mailbox_spec.rb b/spec/mailboxes/application_mailbox_spec.rb index d5e15dc46..733ec5523 100644 --- a/spec/mailboxes/application_mailbox_spec.rb +++ b/spec/mailboxes/application_mailbox_spec.rb @@ -10,6 +10,8 @@ RSpec.describe ApplicationMailbox do let(:reply_mail_without_uuid) { create_inbound_email_from_fixture('reply.eml') } let(:reply_mail_with_in_reply_to) { create_inbound_email_from_fixture('in_reply_to.eml') } let(:support_mail) { create_inbound_email_from_fixture('support.eml') } + let(:mail_with_invalid_to_address) { create_inbound_email_from_fixture('mail_with_invalid_to.eml') } + let(:mail_with_invalid_to_address_2) { create_inbound_email_from_fixture('mail_with_invalid_to_2.eml') } describe 'Default' do it 'catchall mails route to Default Mailbox' do @@ -65,5 +67,21 @@ RSpec.describe ApplicationMailbox do described_class.route reply_cc_mail end end + + describe 'Invalid Mail To Address' do + it 'raises error when mail.to header is malformed' do + expect do + described_class.route mail_with_invalid_to_address + end.to raise_error(StandardError, + 'Invalid email to address header vishnu@chatwoot.com') + end + + it 'raises another error when mail.to header is malformed' do + expect do + described_class.route mail_with_invalid_to_address_2 + end.to raise_error(StandardError, + 'Invalid email to address header vishnu@chatwoot.com www.chatwoot.com') + end + end end end From cc47ccaa2c6dbacb2072e4c8c859322b63c9cc02 Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Wed, 21 Feb 2024 12:33:22 +0530 Subject: [PATCH 11/65] feat(ee): Add SLA management UI (#8777) Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Muhsin Keloth Co-authored-by: Pranav --- app/javascript/dashboard/api/sla.js | 9 ++ .../layout/config/sidebarItems/settings.js | 10 ++ app/javascript/dashboard/featureFlags.js | 1 + .../helper/AnalyticsHelper/events.js | 6 + .../dashboard/i18n/locale/en/index.js | 2 + .../dashboard/i18n/locale/en/settings.json | 1 + .../dashboard/i18n/locale/en/sla.json | 63 ++++++++ .../dashboard/settings/settings.routes.js | 2 + .../routes/dashboard/settings/sla/AddSLA.vue | 50 +++++++ .../routes/dashboard/settings/sla/EditSLA.vue | 60 ++++++++ .../routes/dashboard/settings/sla/Index.vue | 135 +++++++++++++++++ .../routes/dashboard/settings/sla/SlaForm.vue | 141 ++++++++++++++++++ .../dashboard/settings/sla/sla.routes.js | 32 ++++ .../sla/specs/validationMixin.spec.js | 65 ++++++++ .../dashboard/settings/sla/validationMixin.js | 15 ++ .../dashboard/settings/sla/validations.js | 8 + app/javascript/dashboard/store/index.js | 2 + app/javascript/dashboard/store/modules/sla.js | 86 +++++++++++ .../dashboard/store/mutation-types.js | 7 + config/features.yml | 3 + enterprise/config/premium_features.yml | 1 + 21 files changed, 699 insertions(+) create mode 100644 app/javascript/dashboard/api/sla.js create mode 100644 app/javascript/dashboard/i18n/locale/en/sla.json create mode 100644 app/javascript/dashboard/routes/dashboard/settings/sla/AddSLA.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/sla/EditSLA.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/sla/Index.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/sla/SlaForm.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/sla/sla.routes.js create mode 100644 app/javascript/dashboard/routes/dashboard/settings/sla/specs/validationMixin.spec.js create mode 100644 app/javascript/dashboard/routes/dashboard/settings/sla/validationMixin.js create mode 100644 app/javascript/dashboard/routes/dashboard/settings/sla/validations.js create mode 100644 app/javascript/dashboard/store/modules/sla.js diff --git a/app/javascript/dashboard/api/sla.js b/app/javascript/dashboard/api/sla.js new file mode 100644 index 000000000..8480b1846 --- /dev/null +++ b/app/javascript/dashboard/api/sla.js @@ -0,0 +1,9 @@ +import ApiClient from './ApiClient'; + +class SlaAPI extends ApiClient { + constructor() { + super('sla_policies', { accountScoped: true }); + } +} + +export default new SlaAPI(); diff --git a/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js b/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js index 75f09e9da..444285657 100644 --- a/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js +++ b/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js @@ -39,6 +39,7 @@ const settings = accountId => ({ 'settings_teams_finish', 'settings_teams_list', 'settings_teams_new', + 'sla_list', ], menuItems: [ { @@ -158,6 +159,15 @@ const settings = accountId => ({ featureFlag: FEATURE_FLAGS.AUDIT_LOGS, beta: true, }, + { + icon: 'key', + label: 'SLA', + hasSubMenu: false, + toState: frontendURL(`accounts/${accountId}/settings/sla/list`), + toStateName: 'sla_list', + featureFlag: FEATURE_FLAGS.SLA, + beta: true, + }, ], }); diff --git a/app/javascript/dashboard/featureFlags.js b/app/javascript/dashboard/featureFlags.js index 9d06f15c6..2936d22ea 100644 --- a/app/javascript/dashboard/featureFlags.js +++ b/app/javascript/dashboard/featureFlags.js @@ -18,4 +18,5 @@ export const FEATURE_FLAGS = { AUDIT_LOGS: 'audit_logs', INSERT_ARTICLE_IN_REPLY: 'insert_article_in_reply', INBOX_VIEW: 'inbox_view', + SLA: 'sla', }; diff --git a/app/javascript/dashboard/helper/AnalyticsHelper/events.js b/app/javascript/dashboard/helper/AnalyticsHelper/events.js index 59361e446..4b378c74a 100644 --- a/app/javascript/dashboard/helper/AnalyticsHelper/events.js +++ b/app/javascript/dashboard/helper/AnalyticsHelper/events.js @@ -111,3 +111,9 @@ export const INBOX_EVENTS = Object.freeze({ DELETE_NOTIFICATION: 'Deleted notification', DELETE_ALL_NOTIFICATIONS: 'Deleted all notifications', }); + +export const SLA_EVENTS = Object.freeze({ + CREATE: 'Created an SLA', + UPDATE: 'Updated an SLA', + DELETED: 'Deleted an SLA', +}); diff --git a/app/javascript/dashboard/i18n/locale/en/index.js b/app/javascript/dashboard/i18n/locale/en/index.js index 6cc73c5b6..75cdb5836 100644 --- a/app/javascript/dashboard/i18n/locale/en/index.js +++ b/app/javascript/dashboard/i18n/locale/en/index.js @@ -29,6 +29,7 @@ import settings from './settings.json'; import signup from './signup.json'; import teamsSettings from './teamsSettings.json'; import whatsappTemplates from './whatsappTemplates.json'; +import sla from './sla.json'; import inbox from './inbox.json'; export default { @@ -61,6 +62,7 @@ export default { ...setNewPassword, ...settings, ...signup, + ...sla, ...teamsSettings, ...whatsappTemplates, ...inbox, diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json index 6c4d8cbfe..d1d16a607 100644 --- a/app/javascript/dashboard/i18n/locale/en/settings.json +++ b/app/javascript/dashboard/i18n/locale/en/settings.json @@ -238,6 +238,7 @@ "REPORTS_INBOX": "Inbox", "REPORTS_TEAM": "Team", "SET_AVAILABILITY_TITLE": "Set yourself as", + "SLA": "SLA", "BETA": "Beta", "REPORTS_OVERVIEW": "Overview", "FACEBOOK_REAUTHORIZE": "Your Facebook connection has expired, please reconnect your Facebook page to continue services", diff --git a/app/javascript/dashboard/i18n/locale/en/sla.json b/app/javascript/dashboard/i18n/locale/en/sla.json new file mode 100644 index 000000000..67ec4549f --- /dev/null +++ b/app/javascript/dashboard/i18n/locale/en/sla.json @@ -0,0 +1,63 @@ +{ + "SLA": { + "HEADER": "SLA", + "HEADER_BTN_TXT": "Add SLA", + "LOADING": "Fetching SLAs", + "SEARCH_404": "There are no items matching this query", + "SIDEBAR_TXT": "

SLA

Think of Service Level Agreements (SLAs) like friendly promises between a service provider and a customer.

These promises set clear expectations for things like how quickly the team will respond to issues, making sure you always get a reliable and top-notch experience!

", + "LIST": { + "404": "There are no SLAs available in this account.", + "TITLE": "Manage SLA", + "DESC": "SLAs: Friendly promises for great service!", + "TABLE_HEADER": ["Name", "Description", "FRT", "NRT", "RT", "Business Hours"] + }, + "FORM": { + "NAME": { + "LABEL": "SLA Name", + "PLACEHOLDER": "SLA Name", + "REQUIRED_ERROR": "SLA name is required", + "MINIMUM_LENGTH_ERROR": "Minimum length 2 is required", + "VALID_ERROR": "Only Alphabets, Numbers, Hyphen and Underscore are allowed" + }, + "DESCRIPTION": { + "LABEL": "Description", + "PLACEHOLDER": "SLA for premium customers" + }, + "FIRST_RESPONSE_TIME": { + "LABEL": "First Response Time(Seconds)", + "PLACEHOLDER": "300 for 5 minutes" + }, + "NEXT_RESPONSE_TIME": { + "LABEL": "Next Response Time(Seconds)", + "PLACEHOLDER": "600 for 10 minutes" + }, + "RESOLUTION_TIME": { + "LABEL": "Resolution Time(Seconds)", + "PLACEHOLDER": "86400 for 1 day" + }, + "BUSINESS_HOURS": { + "LABEL": "Business Hours", + "PLACEHOLDER": "Only during business hours" + }, + "EDIT": "Edit", + "CREATE": "Create", + "DELETE": "Delete", + "CANCEL": "Cancel" + }, + "ADD": { + "TITLE": "Add SLA", + "DESC": "SLAs: Friendly promises for great service!", + "API": { + "SUCCESS_MESSAGE": "SLA added successfully", + "ERROR_MESSAGE": "There was an error, please try again" + } + }, + "EDIT": { + "TITLE": "Edit SLA", + "API": { + "SUCCESS_MESSAGE": "SLA updated successfully", + "ERROR_MESSAGE": "There was an error, please try again" + } + } + } +} diff --git a/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js b/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js index 48ef316ef..98b81f47c 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js @@ -16,6 +16,7 @@ import macros from './macros/macros.routes'; import profile from './profile/profile.routes'; import reports from './reports/reports.routes'; import store from '../../../store'; +import sla from './sla/sla.routes'; import teams from './teams/teams.routes'; export default { @@ -47,6 +48,7 @@ export default { ...macros.routes, ...profile.routes, ...reports.routes, + ...sla.routes, ...teams.routes, ], }; diff --git a/app/javascript/dashboard/routes/dashboard/settings/sla/AddSLA.vue b/app/javascript/dashboard/routes/dashboard/settings/sla/AddSLA.vue new file mode 100644 index 000000000..772ec4bcd --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/sla/AddSLA.vue @@ -0,0 +1,50 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/sla/EditSLA.vue b/app/javascript/dashboard/routes/dashboard/settings/sla/EditSLA.vue new file mode 100644 index 000000000..2fc577bad --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/sla/EditSLA.vue @@ -0,0 +1,60 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/sla/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/sla/Index.vue new file mode 100644 index 000000000..473583f6d --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/sla/Index.vue @@ -0,0 +1,135 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/sla/SlaForm.vue b/app/javascript/dashboard/routes/dashboard/settings/sla/SlaForm.vue new file mode 100644 index 000000000..775c0ff2a --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/sla/SlaForm.vue @@ -0,0 +1,141 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/sla/sla.routes.js b/app/javascript/dashboard/routes/dashboard/settings/sla/sla.routes.js new file mode 100644 index 000000000..360c5a1f5 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/sla/sla.routes.js @@ -0,0 +1,32 @@ +import { frontendURL } from '../../../../helper/URLHelper'; + +const SettingsContent = () => import('../Wrapper.vue'); +const Index = () => import('./Index.vue'); + +export default { + routes: [ + { + path: frontendURL('accounts/:accountId/settings/sla'), + component: SettingsContent, + props: { + headerTitle: 'SLA.HEADER', + icon: 'tag', + showNewButton: true, + }, + children: [ + { + path: '', + name: 'sla_wrapper', + roles: ['administrator'], + redirect: 'list', + }, + { + path: 'list', + name: 'sla_list', + roles: ['administrator'], + component: Index, + }, + ], + }, + ], +}; diff --git a/app/javascript/dashboard/routes/dashboard/settings/sla/specs/validationMixin.spec.js b/app/javascript/dashboard/routes/dashboard/settings/sla/specs/validationMixin.spec.js new file mode 100644 index 000000000..a828c8556 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/sla/specs/validationMixin.spec.js @@ -0,0 +1,65 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import VueI18n from 'vue-i18n'; +import Vuelidate from 'vuelidate'; + +import validationMixin from '../validationMixin'; +import validations from '../validations'; +import i18n from 'dashboard/i18n'; + +const localVue = createLocalVue(); +localVue.use(VueI18n); +localVue.use(Vuelidate); + +const i18nConfig = new VueI18n({ + locale: 'en', + messages: i18n, +}); + +const TestComponent = { + render() {}, + mixins: [validationMixin], + validations, +}; + +describe('validationMixin', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallowMount(TestComponent, { + localVue, + i18n: i18nConfig, + data() { + return { + name: '', + }; + }, + }); + }); + + it('should not return required error message if name is empty but not touched', () => { + wrapper.setData({ name: '' }); + expect(wrapper.vm.getSlaNameErrorMessage).toBe(''); + }); + + it('should return empty error message if name is valid', () => { + wrapper.setData({ name: 'ValidName' }); + wrapper.vm.$v.name.$touch(); + expect(wrapper.vm.getSlaNameErrorMessage).toBe(''); + }); + + it('should return required error message if name is empty', () => { + wrapper.setData({ name: '' }); + wrapper.vm.$v.name.$touch(); + expect(wrapper.vm.getSlaNameErrorMessage).toBe( + wrapper.vm.$t('SLA.FORM.NAME.REQUIRED_ERROR') + ); + }); + + it('should return minimum length error message if name is too short', () => { + wrapper.setData({ name: 'a' }); + wrapper.vm.$v.name.$touch(); + expect(wrapper.vm.getSlaNameErrorMessage).toBe( + wrapper.vm.$t('SLA.FORM.NAME.MINIMUM_LENGTH_ERROR') + ); + }); +}); diff --git a/app/javascript/dashboard/routes/dashboard/settings/sla/validationMixin.js b/app/javascript/dashboard/routes/dashboard/settings/sla/validationMixin.js new file mode 100644 index 000000000..28f7436c8 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/sla/validationMixin.js @@ -0,0 +1,15 @@ +export default { + computed: { + getSlaNameErrorMessage() { + let errorMessage = ''; + if (this.$v.name.$error) { + if (!this.$v.name.required) { + errorMessage = this.$t('SLA.FORM.NAME.REQUIRED_ERROR'); + } else if (!this.$v.name.minLength) { + errorMessage = this.$t('SLA.FORM.NAME.MINIMUM_LENGTH_ERROR'); + } + } + return errorMessage; + }, + }, +}; diff --git a/app/javascript/dashboard/routes/dashboard/settings/sla/validations.js b/app/javascript/dashboard/routes/dashboard/settings/sla/validations.js new file mode 100644 index 000000000..bda4b9556 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/sla/validations.js @@ -0,0 +1,8 @@ +import { required, minLength } from 'vuelidate/lib/validators'; + +export default { + name: { + required, + minLength: minLength(2), + }, +}; diff --git a/app/javascript/dashboard/store/index.js b/app/javascript/dashboard/store/index.js index 2d1564b79..91b84ddd1 100755 --- a/app/javascript/dashboard/store/index.js +++ b/app/javascript/dashboard/store/index.js @@ -38,6 +38,7 @@ import macros from './modules/macros'; import notifications from './modules/notifications'; import portals from './modules/helpCenterPortals'; import reports from './modules/reports'; +import sla from './modules/sla'; import teamMembers from './modules/teamMembers'; import teams from './modules/teams'; import userNotificationSettings from './modules/userNotificationSettings'; @@ -109,6 +110,7 @@ export default new Vuex.Store({ userNotificationSettings, webhooks, draftMessages, + sla, }, plugins, }); diff --git a/app/javascript/dashboard/store/modules/sla.js b/app/javascript/dashboard/store/modules/sla.js new file mode 100644 index 000000000..e1ded7f51 --- /dev/null +++ b/app/javascript/dashboard/store/modules/sla.js @@ -0,0 +1,86 @@ +import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers'; +import types from '../mutation-types'; +import SlaAPI from '../../api/sla'; +import AnalyticsHelper from '../../helper/AnalyticsHelper'; +import { SLA_EVENTS } from '../../helper/AnalyticsHelper/events'; +import { throwErrorMessage } from '../utils/api'; + +export const state = { + records: [], + uiFlags: { + isFetching: false, + isFetchingItem: false, + isCreating: false, + isDeleting: false, + }, +}; + +export const getters = { + getSLA(_state) { + return _state.records; + }, + getUIFlags(_state) { + return _state.uiFlags; + }, +}; + +export const actions = { + get: async function get({ commit }) { + commit(types.SET_SLA_UI_FLAG, { isFetching: true }); + try { + const response = await SlaAPI.get(); + commit(types.SET_SLA, response.data.payload); + } catch (error) { + // Ignore error + } finally { + commit(types.SET_SLA_UI_FLAG, { isFetching: false }); + } + }, + + create: async function create({ commit }, slaObj) { + commit(types.SET_SLA_UI_FLAG, { isCreating: true }); + try { + const response = await SlaAPI.create(slaObj); + AnalyticsHelper.track(SLA_EVENTS.CREATE); + commit(types.ADD_SLA, response.data.payload); + } catch (error) { + throwErrorMessage(error); + } finally { + commit(types.SET_SLA_UI_FLAG, { isCreating: false }); + } + }, + + update: async function update({ commit }, { id, ...updateObj }) { + commit(types.SET_SLA_UI_FLAG, { isUpdating: true }); + try { + const response = await SlaAPI.update(id, updateObj); + AnalyticsHelper.track(SLA_EVENTS.UPDATE); + commit(types.EDIT_SLA, response.data.payload); + } catch (error) { + throwErrorMessage(error); + } finally { + commit(types.SET_SLA_UI_FLAG, { isUpdating: false }); + } + }, +}; + +export const mutations = { + [types.SET_SLA_UI_FLAG](_state, data) { + _state.uiFlags = { + ..._state.uiFlags, + ...data, + }; + }, + + [types.SET_SLA]: MutationHelpers.set, + [types.ADD_SLA]: MutationHelpers.create, + [types.EDIT_SLA]: MutationHelpers.update, +}; + +export default { + namespaced: true, + state, + getters, + actions, + mutations, +}; diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index 0d4f0c010..c9d2d1767 100644 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -301,4 +301,11 @@ export default { SET_AUDIT_LOGS_UI_FLAG: 'SET_AUDIT_LOGS_UI_FLAG', SET_AUDIT_LOGS: 'SET_AUDIT_LOGS', SET_AUDIT_LOGS_META: 'SET_AUDIT_LOGS_META', + + // SLA + SET_SLA_UI_FLAG: 'SET_SLA_UI_FLAG', + SET_SLA: 'SET_SLA', + ADD_SLA: 'ADD_SLA', + EDIT_SLA: 'EDIT_SLA', + DELETE_SLA: 'DELETE_SLA', }; diff --git a/config/features.yml b/config/features.yml index 529ad37d9..f0477c297 100644 --- a/config/features.yml +++ b/config/features.yml @@ -66,3 +66,6 @@ enabled: false - name: inbox_view enabled: false +- name: sla + enabled: false + premium: true diff --git a/enterprise/config/premium_features.yml b/enterprise/config/premium_features.yml index 9628e1da4..18e5b15ba 100644 --- a/enterprise/config/premium_features.yml +++ b/enterprise/config/premium_features.yml @@ -2,3 +2,4 @@ - disable_branding - audit_logs - response_bot +- sla From ebae547a6083c6b1b557f0f51bd75a565e73361f Mon Sep 17 00:00:00 2001 From: CristianDuta Date: Wed, 21 Feb 2024 13:11:20 +0100 Subject: [PATCH 12/65] feat: Add ability to resolve API channel conversations (#8348) - Create a new endpoint to fetch a single conversation in client apis - Create a new endpoint to resolve a single conversation in client apis - Update swagger API definition to include missing endpoints Fixes: #6329 Co-authored-by: Cristian Duta --- .../v1/inboxes/conversations_controller.rb | 24 ++- .../inboxes/conversations/show.json.jbuilder | 1 + .../conversations/toggle_status.json.jbuilder | 1 + config/routes.rb | 3 +- .../v1/inbox/conversations_controller_spec.rb | 39 +++++ swagger/paths/index.yml | 33 +++- .../public/inboxes/conversations/show.yml | 14 ++ .../inboxes/conversations/toggle_status.yml | 14 ++ .../inboxes/conversations/toggle_typing.yml | 18 +++ .../conversations/update_last_seen.yml | 12 ++ swagger/swagger.json | 143 ++++++++++++++++++ 11 files changed, 298 insertions(+), 4 deletions(-) create mode 100644 app/views/public/api/v1/inboxes/conversations/show.json.jbuilder create mode 100644 app/views/public/api/v1/inboxes/conversations/toggle_status.json.jbuilder create mode 100644 swagger/paths/public/inboxes/conversations/show.yml create mode 100644 swagger/paths/public/inboxes/conversations/toggle_status.yml create mode 100644 swagger/paths/public/inboxes/conversations/toggle_typing.yml create mode 100644 swagger/paths/public/inboxes/conversations/update_last_seen.yml diff --git a/app/controllers/public/api/v1/inboxes/conversations_controller.rb b/app/controllers/public/api/v1/inboxes/conversations_controller.rb index 671863bdc..4e3b5dca9 100644 --- a/app/controllers/public/api/v1/inboxes/conversations_controller.rb +++ b/app/controllers/public/api/v1/inboxes/conversations_controller.rb @@ -1,15 +1,31 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::InboxesController include Events::Types - before_action :set_conversation, only: [:toggle_typing, :update_last_seen] + before_action :set_conversation, only: [:toggle_typing, :update_last_seen, :show, :toggle_status] def index @conversations = @contact_inbox.hmac_verified? ? @contact.conversations : @contact_inbox.conversations end + def show; end + def create @conversation = create_conversation end + def toggle_status + # Check if the conversation is already resolved to prevent redundant operations + return if @conversation.resolved? + + # Assign the conversation's contact as the resolver + # This step attributes the resolution action to the contact involved in the conversation + # If this assignment is not made, the system implicitly becomes the resolver by default + Current.contact = @conversation.contact + + # Update the conversation's status to 'resolved' to reflect its closure + @conversation.status = :resolved + @conversation.save! + end + def toggle_typing case params[:typing_status] when 'on' @@ -30,7 +46,11 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox private def set_conversation - @conversation = @contact_inbox.contact.conversations.find_by!(display_id: params[:id]) + @conversation = if @contact_inbox.hmac_verified? + @contact_inbox.contact.conversations.find_by!(display_id: params[:id]) + else + @contact_inbox.conversations.find_by!(display_id: params[:id]) + end end def create_conversation diff --git a/app/views/public/api/v1/inboxes/conversations/show.json.jbuilder b/app/views/public/api/v1/inboxes/conversations/show.json.jbuilder new file mode 100644 index 000000000..1b9588465 --- /dev/null +++ b/app/views/public/api/v1/inboxes/conversations/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'public/api/v1/models/conversation', formats: [:json], resource: @conversation diff --git a/app/views/public/api/v1/inboxes/conversations/toggle_status.json.jbuilder b/app/views/public/api/v1/inboxes/conversations/toggle_status.json.jbuilder new file mode 100644 index 000000000..1b9588465 --- /dev/null +++ b/app/views/public/api/v1/inboxes/conversations/toggle_status.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'public/api/v1/models/conversation', formats: [:json], resource: @conversation diff --git a/config/routes.rb b/config/routes.rb index 8c4d4124b..1ee74145f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -366,8 +366,9 @@ Rails.application.routes.draw do resources :inboxes do scope module: :inboxes do resources :contacts, only: [:create, :show, :update] do - resources :conversations, only: [:index, :create] do + resources :conversations, only: [:index, :create, :show] do member do + post :toggle_status post :toggle_typing post :update_last_seen end diff --git a/spec/controllers/public/api/v1/inbox/conversations_controller_spec.rb b/spec/controllers/public/api/v1/inbox/conversations_controller_spec.rb index d54f26272..2b2bdefc3 100644 --- a/spec/controllers/public/api/v1/inbox/conversations_controller_spec.rb +++ b/spec/controllers/public/api/v1/inbox/conversations_controller_spec.rb @@ -36,6 +36,45 @@ RSpec.describe 'Public Inbox Contact Conversations API', type: :request do end end + describe 'GET /public/api/v1/inboxes/{identifier}/contact/{source_id}/conversations/{conversation_id}' do + it 'returns the conversation that the contact has access to' do + conversation = create(:conversation, contact_inbox: contact_inbox) + create(:message, account: conversation.account, inbox: conversation.inbox, conversation: conversation, content: 'message-1') + create(:message, account: conversation.account, inbox: conversation.inbox, conversation: conversation, content: 'message-2') + + get "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}/conversations/#{conversation.display_id}" + + expect(response).to have_http_status(:success) + data = response.parsed_body + expect(data['id']).to eq(conversation.display_id) + expect(data['messages']).to be_a(Array) + expect(data['messages'].length).to eq(conversation.messages.count) + expect(data['messages'].pluck('content')).to include(conversation.messages.first.content) + end + end + + describe 'POST /public/api/v1/inboxes/{identifier}/contact/{source_id}/conversations/{conversation_id}/toggle_status' do + it 'resolves the conversation' do + conversation = create(:conversation, contact_inbox: contact_inbox) + display_id = conversation.display_id + + post "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}/conversations/#{display_id}/toggle_status" + + expect(response).to have_http_status(:success) + expect(conversation.reload).to be_resolved + end + + it 'does not resolve a conversation that is already resolved' do + conversation = create(:conversation, contact_inbox: contact_inbox, status: :resolved) + display_id = conversation.display_id + + post "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}/conversations/#{display_id}/toggle_status" + + expect(response).to have_http_status(:success) + expect(Conversation.where(id: conversation.id, status: :resolved).count).to eq(1) + end + end + describe 'POST /public/api/v1/inboxes/{identifier}/contact/{source_id}/conversations' do it 'creates a conversation for that contact' do post "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}/conversations" diff --git a/swagger/paths/index.yml b/swagger/paths/index.yml index d58439647..2c6dd3f87 100644 --- a/swagger/paths/index.yml +++ b/swagger/paths/index.yml @@ -94,7 +94,6 @@ patch: $ref: ./public/inboxes/contacts/update.yml - /public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversations: parameters: - $ref: '#/parameters/public_inbox_identifier' @@ -104,6 +103,38 @@ get: $ref: ./public/inboxes/conversations/index.yml +/public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversations/{conversation_id}: + parameters: + - $ref: '#/parameters/public_inbox_identifier' + - $ref: '#/parameters/public_contact_identifier' + - $ref: '#/parameters/conversation_id' + get: + $ref: ./public/inboxes/conversations/show.yml + +/public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversations/{conversation_id}/toggle_status: + parameters: + - $ref: '#/parameters/public_inbox_identifier' + - $ref: '#/parameters/public_contact_identifier' + - $ref: '#/parameters/conversation_id' + post: + $ref: ./public/inboxes/conversations/toggle_status.yml + +/public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversations/{conversation_id}/toggle_typing: + parameters: + - $ref: '#/parameters/public_inbox_identifier' + - $ref: '#/parameters/public_contact_identifier' + - $ref: '#/parameters/conversation_id' + post: + $ref: ./public/inboxes/conversations/toggle_typing.yml + +/public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversations/{conversation_id}/update_last_seen: + parameters: + - $ref: '#/parameters/public_inbox_identifier' + - $ref: '#/parameters/public_contact_identifier' + - $ref: '#/parameters/conversation_id' + post: + $ref: ./public/inboxes/conversations/update_last_seen.yml + /public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversations/{conversation_id}/messages: parameters: - $ref: '#/parameters/public_inbox_identifier' diff --git a/swagger/paths/public/inboxes/conversations/show.yml b/swagger/paths/public/inboxes/conversations/show.yml new file mode 100644 index 000000000..2ec6caa63 --- /dev/null +++ b/swagger/paths/public/inboxes/conversations/show.yml @@ -0,0 +1,14 @@ +tags: + - Conversations API +operationId: get-single-conversation +summary: Get a single conversation +description: Retrieves the details of a specific conversation +responses: + 200: + description: Success + schema: + $ref: '#/definitions/public_conversation' + 401: + description: Unauthorized + 404: + description: Conversation not found diff --git a/swagger/paths/public/inboxes/conversations/toggle_status.yml b/swagger/paths/public/inboxes/conversations/toggle_status.yml new file mode 100644 index 000000000..cac1c3d8d --- /dev/null +++ b/swagger/paths/public/inboxes/conversations/toggle_status.yml @@ -0,0 +1,14 @@ +tags: + - Conversations API +operationId: resolve-conversation +summary: Resolve a conversation +description: Marks a conversation as resolved +responses: + 200: + description: Conversation resolved successfully + schema: + $ref: '#/definitions/public_conversation' + 401: + description: Unauthorized + 404: + description: Conversation not found diff --git a/swagger/paths/public/inboxes/conversations/toggle_typing.yml b/swagger/paths/public/inboxes/conversations/toggle_typing.yml new file mode 100644 index 000000000..af01c77f6 --- /dev/null +++ b/swagger/paths/public/inboxes/conversations/toggle_typing.yml @@ -0,0 +1,18 @@ +tags: + - Conversations API +operationId: toggle-typing-status +summary: Toggle typing status +description: Toggles the typing status in a conversation +parameters: + - name: typing_status + in: query + required: true + type: string + description: Typing status, either 'on' or 'off' +responses: + 200: + description: Typing status toggled successfully + 401: + description: Unauthorized + 404: + description: Conversation not found diff --git a/swagger/paths/public/inboxes/conversations/update_last_seen.yml b/swagger/paths/public/inboxes/conversations/update_last_seen.yml new file mode 100644 index 000000000..4e56aa11d --- /dev/null +++ b/swagger/paths/public/inboxes/conversations/update_last_seen.yml @@ -0,0 +1,12 @@ +tags: + - Conversations API +operationId: update-last-seen +summary: Update last seen +description: Updates the last seen time of the contact in a conversation +responses: + 200: + description: Last seen updated successfully + 401: + description: Unauthorized + 404: + description: Conversation not found diff --git a/swagger/swagger.json b/swagger/swagger.json index 4859a785f..dde7c5f01 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -894,6 +894,149 @@ } } }, + "/public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversations/{conversation_id}": { + "parameters": [ + { + "$ref": "#/parameters/public_inbox_identifier" + }, + { + "$ref": "#/parameters/public_contact_identifier" + }, + { + "$ref": "#/parameters/conversation_id" + } + ], + "get": { + "tags": [ + "Conversations API" + ], + "operationId": "get-single-conversation", + "summary": "Get a single conversation", + "description": "Retrieves the details of a specific conversation", + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/public_conversation" + } + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "Conversation not found" + } + } + } + }, + "/public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversations/{conversation_id}/toggle_status": { + "parameters": [ + { + "$ref": "#/parameters/public_inbox_identifier" + }, + { + "$ref": "#/parameters/public_contact_identifier" + }, + { + "$ref": "#/parameters/conversation_id" + } + ], + "post": { + "tags": [ + "Conversations API" + ], + "operationId": "resolve-conversation", + "summary": "Resolve a conversation", + "description": "Marks a conversation as resolved", + "responses": { + "200": { + "description": "Conversation resolved successfully", + "schema": { + "$ref": "#/definitions/public_conversation" + } + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "Conversation not found" + } + } + } + }, + "/public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversations/{conversation_id}/toggle_typing": { + "parameters": [ + { + "$ref": "#/parameters/public_inbox_identifier" + }, + { + "$ref": "#/parameters/public_contact_identifier" + }, + { + "$ref": "#/parameters/conversation_id" + } + ], + "post": { + "tags": [ + "Conversations API" + ], + "operationId": "toggle-typing-status", + "summary": "Toggle typing status", + "description": "Toggles the typing status in a conversation", + "parameters": [ + { + "name": "typing_status", + "in": "query", + "required": true, + "type": "string", + "description": "Typing status, either 'on' or 'off'" + } + ], + "responses": { + "200": { + "description": "Typing status toggled successfully" + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "Conversation not found" + } + } + } + }, + "/public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversations/{conversation_id}/update_last_seen": { + "parameters": [ + { + "$ref": "#/parameters/public_inbox_identifier" + }, + { + "$ref": "#/parameters/public_contact_identifier" + }, + { + "$ref": "#/parameters/conversation_id" + } + ], + "post": { + "tags": [ + "Conversations API" + ], + "operationId": "update-last-seen", + "summary": "Update last seen", + "description": "Updates the last seen time of the contact in a conversation", + "responses": { + "200": { + "description": "Last seen updated successfully" + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "Conversation not found" + } + } + } + }, "/public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversations/{conversation_id}/messages": { "parameters": [ { From c031cb19d26595377bb15b9c1cd9e3fa02d74366 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 21 Feb 2024 18:51:00 +0530 Subject: [PATCH 13/65] fix: downcase email before finding (#8921) * fix: downcase email when finding * feat: add `from_email` class * refactor: use `from_email` * feat: add rule to disallow find_by email directly * chore: remove redundant test Since the previous imlpmentation didn't do a case-insentive search, a new user would be created, and the error would be raised at the DB layer. With the new changes, this test case is redundant * refactor: use from_email --- .rubocop.yml | 5 +++++ app/actions/contact_identify_action.rb | 2 +- app/builders/agent_builder.rb | 2 +- .../contact_inbox_with_contact_builder.rb | 2 +- .../devise_overrides/passwords_controller.rb | 2 +- .../devise_overrides/sessions_controller.rb | 2 +- .../platform/api/v1/users_controller.rb | 2 +- app/mailboxes/imap/imap_mailbox.rb | 2 +- app/mailboxes/support_mailbox.rb | 2 +- app/models/contact.rb | 6 ++++++ app/models/user.rb | 6 ++++++ app/services/data_import/contact_manager.rb | 2 +- lib/integrations/slack/slack_message_helper.rb | 2 +- lib/rubocop/user_find_by.rb | 16 ++++++++++++++++ lib/seeders/account_seeder.rb | 4 ++-- .../platform/api/v1/users_controller_spec.rb | 5 ----- spec/jobs/data_import_job_spec.rb | 2 +- 17 files changed, 46 insertions(+), 18 deletions(-) create mode 100644 lib/rubocop/user_find_by.rb diff --git a/.rubocop.yml b/.rubocop.yml index 5add6e26a..f4187dbb5 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -2,6 +2,7 @@ require: - rubocop-performance - rubocop-rails - rubocop-rspec + - ./lib/rubocop/user_find_by.rb Layout/LineLength: Max: 150 @@ -140,6 +141,10 @@ RSpec/MultipleExpectations: RSpec/MultipleMemoizedHelpers: Max: 14 +# custom rules +UseFromEmail: + Enabled: true + AllCops: NewCops: enable Exclude: diff --git a/app/actions/contact_identify_action.rb b/app/actions/contact_identify_action.rb index 6b2f37433..a88d3535b 100644 --- a/app/actions/contact_identify_action.rb +++ b/app/actions/contact_identify_action.rb @@ -59,7 +59,7 @@ class ContactIdentifyAction def existing_email_contact return if params[:email].blank? - @existing_email_contact ||= account.contacts.find_by(email: params[:email]) + @existing_email_contact ||= account.contacts.from_email(params[:email]) end def existing_phone_number_contact diff --git a/app/builders/agent_builder.rb b/app/builders/agent_builder.rb index 6ea68821d..07b0e7345 100644 --- a/app/builders/agent_builder.rb +++ b/app/builders/agent_builder.rb @@ -27,7 +27,7 @@ class AgentBuilder # Finds a user by email or creates a new one with a temporary password. # @return [User] the found or created user. def find_or_create_user - user = User.find_by(email: email) + user = User.from_email(email) return user if user temp_password = "1!aA#{SecureRandom.alphanumeric(12)}" diff --git a/app/builders/contact_inbox_with_contact_builder.rb b/app/builders/contact_inbox_with_contact_builder.rb index d97f64cfe..6e913eda8 100644 --- a/app/builders/contact_inbox_with_contact_builder.rb +++ b/app/builders/contact_inbox_with_contact_builder.rb @@ -75,7 +75,7 @@ class ContactInboxWithContactBuilder def find_contact_by_email(email) return if email.blank? - account.contacts.find_by(email: email.downcase) + account.contacts.from_email(email) end def find_contact_by_phone_number(phone_number) diff --git a/app/controllers/devise_overrides/passwords_controller.rb b/app/controllers/devise_overrides/passwords_controller.rb index 26a9d4555..17dd32086 100644 --- a/app/controllers/devise_overrides/passwords_controller.rb +++ b/app/controllers/devise_overrides/passwords_controller.rb @@ -5,7 +5,7 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController skip_before_action :authenticate_user!, raise: false def create - @user = User.find_by(email: params[:email]) + @user = User.from_email(params[:email]) if @user @user.send_reset_password_instructions build_response(I18n.t('messages.reset_password_success'), 200) diff --git a/app/controllers/devise_overrides/sessions_controller.rb b/app/controllers/devise_overrides/sessions_controller.rb index 2659aeebf..e623e52f7 100644 --- a/app/controllers/devise_overrides/sessions_controller.rb +++ b/app/controllers/devise_overrides/sessions_controller.rb @@ -33,7 +33,7 @@ class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController def process_sso_auth_token return if params[:email].blank? - user = User.find_by(email: params[:email]) + user = User.from_email(params[:email]) @resource = user if user&.valid_sso_auth_token?(params[:sso_auth_token]) end end diff --git a/app/controllers/platform/api/v1/users_controller.rb b/app/controllers/platform/api/v1/users_controller.rb index 03ed55754..453e475b0 100644 --- a/app/controllers/platform/api/v1/users_controller.rb +++ b/app/controllers/platform/api/v1/users_controller.rb @@ -8,7 +8,7 @@ class Platform::Api::V1::UsersController < PlatformController def show; end def create - @resource = (User.find_by(email: user_params[:email]) || User.new(user_params)) + @resource = (User.from_email(user_params[:email]) || User.new(user_params)) @resource.skip_confirmation! @resource.save! @platform_app.platform_app_permissibles.find_or_create_by!(permissible: @resource) diff --git a/app/mailboxes/imap/imap_mailbox.rb b/app/mailboxes/imap/imap_mailbox.rb index 57d30253a..a8308b48a 100644 --- a/app/mailboxes/imap/imap_mailbox.rb +++ b/app/mailboxes/imap/imap_mailbox.rb @@ -91,7 +91,7 @@ class Imap::ImapMailbox end def find_or_create_contact - @contact = @inbox.contacts.find_by(email: @processed_mail.original_sender) + @contact = @inbox.contacts.from_email(@processed_mail.original_sender) if @contact.present? @contact_inbox = ContactInbox.find_by(inbox: @inbox, contact: @contact) else diff --git a/app/mailboxes/support_mailbox.rb b/app/mailboxes/support_mailbox.rb index 5a1f5ecf5..94404d8ed 100644 --- a/app/mailboxes/support_mailbox.rb +++ b/app/mailboxes/support_mailbox.rb @@ -86,7 +86,7 @@ class SupportMailbox < ApplicationMailbox end def find_or_create_contact - @contact = @inbox.contacts.find_by(email: original_sender_email) + @contact = @inbox.contacts.from_email(original_sender_email) if @contact.present? @contact_inbox = ContactInbox.find_by(inbox: @inbox, contact: @contact) else diff --git a/app/models/contact.rb b/app/models/contact.rb index af3f605a7..23eca55e6 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -165,6 +165,12 @@ class Contact < ApplicationRecord email_format end + def self.from_email(email) + # rubocop:disable UseFromEmail,Migration/DepartmentName + find_by(email: email.downcase) + # rubocop:enable UseFromEmail,Migration/DepartmentName + end + private def ip_lookup diff --git a/app/models/user.rb b/app/models/user.rb index ba638f637..b54f74a64 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -156,6 +156,12 @@ class User < ApplicationRecord mutations_from_database.changed?('email') end + def self.from_email(email) + # rubocop:disable UseFromEmail,Migration/DepartmentName + find_by(email: email.downcase) + # rubocop:enable UseFromEmail,Migration/DepartmentName + end + private def remove_macros diff --git a/app/services/data_import/contact_manager.rb b/app/services/data_import/contact_manager.rb index ab9a1ee70..460a83726 100644 --- a/app/services/data_import/contact_manager.rb +++ b/app/services/data_import/contact_manager.rb @@ -35,7 +35,7 @@ class DataImport::ContactManager def find_contact_by_email(params) return unless params[:email] - @account.contacts.find_by(email: params[:email]) + @account.contacts.from_email(params[:email]) end def find_contact_by_phone_number(params) diff --git a/lib/integrations/slack/slack_message_helper.rb b/lib/integrations/slack/slack_message_helper.rb index 9522112a8..762d488c5 100644 --- a/lib/integrations/slack/slack_message_helper.rb +++ b/lib/integrations/slack/slack_message_helper.rb @@ -69,7 +69,7 @@ module Integrations::Slack::SlackMessageHelper def sender user_email = slack_client.users_info(user: params[:event][:user])[:user][:profile][:email] - conversation.account.users.find_by(email: user_email) + conversation.account.users.from_email(user_email) end def private_note? diff --git a/lib/rubocop/user_find_by.rb b/lib/rubocop/user_find_by.rb new file mode 100644 index 000000000..0bc248c27 --- /dev/null +++ b/lib/rubocop/user_find_by.rb @@ -0,0 +1,16 @@ +require 'rubocop' + +# Enforces use of from_email for email attribute lookups +class UseFromEmail < RuboCop::Cop::Cop + MSG = 'Use `from_email` for email lookups to ensure case insensitivity.'.freeze + + def_node_matcher :find_by_email?, <<~PATTERN + (send _ :find_by (hash (pair (sym :email) _))) + PATTERN + + def on_send(node) + return unless find_by_email?(node) + + add_offense(node, message: MSG) + end +end diff --git a/lib/seeders/account_seeder.rb b/lib/seeders/account_seeder.rb index 9793ffd1d..802a02dbf 100644 --- a/lib/seeders/account_seeder.rb +++ b/lib/seeders/account_seeder.rb @@ -88,7 +88,7 @@ class Seeders::AccountSeeder end def create_conversation(contact_inbox:, conversation_data:) - assignee = User.find_by(email: conversation_data['assignee']) if conversation_data['assignee'].present? + assignee = User.from_email(conversation_data['assignee']) if conversation_data['assignee'].present? conversation = contact_inbox.conversations.create!(account: contact_inbox.inbox.account, contact: contact_inbox.contact, inbox: contact_inbox.inbox, assignee: assignee) create_messages(conversation: conversation, messages: conversation_data['messages']) @@ -111,7 +111,7 @@ class Seeders::AccountSeeder if message_data['message_type'] == 'incoming' conversation.contact elsif message_data['sender'].present? - User.find_by(email: message_data['sender']) + User.from_email(message_data['sender']) end end diff --git a/spec/controllers/platform/api/v1/users_controller_spec.rb b/spec/controllers/platform/api/v1/users_controller_spec.rb index 2ebb0d1a4..95d6cd452 100644 --- a/spec/controllers/platform/api/v1/users_controller_spec.rb +++ b/spec/controllers/platform/api/v1/users_controller_spec.rb @@ -115,11 +115,6 @@ RSpec.describe 'Platform Users API', type: :request do ) ) expect(platform_app.platform_app_permissibles.first.permissible_id).to eq data['id'] - - post '/platform/api/v1/users/', params: { name: 'test', email: 'TesT@test.com', password: 'Password1!' }, - headers: { api_access_token: platform_app.access_token.token }, as: :json - data = response.parsed_body - expect(data['message']).to eq('Email has already been taken') end it 'fetch existing user and creates permissible for the user' do diff --git a/spec/jobs/data_import_job_spec.rb b/spec/jobs/data_import_job_spec.rb index 5ece07e53..fb1057f4a 100644 --- a/spec/jobs/data_import_job_spec.rb +++ b/spec/jobs/data_import_job_spec.rb @@ -111,7 +111,7 @@ RSpec.describe DataImportJob do described_class.perform_now(existing_data_import) expect(existing_data_import.account.contacts.count).to eq(csv_length) - contact = Contact.find_by(email: csv_data[0]['email']) + contact = Contact.from_email(csv_data[0]['email']) expect(contact).to be_present expect(contact.phone_number).to eq("+#{csv_data[0]['phone_number']}") expect(contact.name).to eq((csv_data[0]['name']).to_s) From ae4c8d818fe038aff5e1dac5fc6ea457c48f4851 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Thu, 22 Feb 2024 03:48:42 +0530 Subject: [PATCH 14/65] feat: Ability to block contacts permanently (#8922) Co-authored-by: Pranav --- .../api/v1/accounts/contacts_controller.rb | 2 +- .../conversation/specs/MoreActions.spec.js | 4 ++-- .../dashboard/i18n/locale/en/contact.json | 8 +++---- .../concerns/conversation_mute_helpers.rb | 16 +++----------- app/models/contact.rb | 2 ++ app/models/conversation.rb | 7 ++++-- .../20240213131252_add_blocked_to_contacts.rb | 6 +++++ db/schema.rb | 3 +++ .../v1/accounts/contacts_controller_spec.rb | 21 ++++++++++++++++++ spec/models/conversation_spec.rb | 22 ++++++++++++++----- 10 files changed, 63 insertions(+), 28 deletions(-) create mode 100644 db/migrate/20240213131252_add_blocked_to_contacts.rb diff --git a/app/controllers/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb index 98683ea25..71e9100e7 100644 --- a/app/controllers/api/v1/accounts/contacts_controller.rb +++ b/app/controllers/api/v1/accounts/contacts_controller.rb @@ -148,7 +148,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController end def permitted_params - params.permit(:name, :identifier, :email, :phone_number, :avatar, :avatar_url, additional_attributes: {}, custom_attributes: {}) + params.permit(:name, :identifier, :email, :phone_number, :avatar, :blocked, :avatar_url, additional_attributes: {}, custom_attributes: {}) end def contact_custom_attributes diff --git a/app/javascript/dashboard/components/widgets/conversation/specs/MoreActions.spec.js b/app/javascript/dashboard/components/widgets/conversation/specs/MoreActions.spec.js index dafe97cba..df3c85f1f 100644 --- a/app/javascript/dashboard/components/widgets/conversation/specs/MoreActions.spec.js +++ b/app/javascript/dashboard/components/widgets/conversation/specs/MoreActions.spec.js @@ -78,7 +78,7 @@ describe('MoveActions', () => { expect(window.bus.$emit).toBeCalledWith( 'newToastMessage', - 'This conversation is muted for 6 hours', + 'This contact is blocked successfully. You will not be notified of any future conversations.', undefined ); }); @@ -104,7 +104,7 @@ describe('MoveActions', () => { expect(window.bus.$emit).toBeCalledWith( 'newToastMessage', - 'This conversation is unmuted', + 'This contact is unblocked successfully.', undefined ); }); diff --git a/app/javascript/dashboard/i18n/locale/en/contact.json b/app/javascript/dashboard/i18n/locale/en/contact.json index cae908016..791279899 100644 --- a/app/javascript/dashboard/i18n/locale/en/contact.json +++ b/app/javascript/dashboard/i18n/locale/en/contact.json @@ -39,10 +39,10 @@ }, "MERGE_CONTACT": "Merge contact", "CONTACT_ACTIONS": "Contact actions", - "MUTE_CONTACT": "Mute Conversation", - "UNMUTE_CONTACT": "Unmute Conversation", - "MUTED_SUCCESS": "This conversation is muted for 6 hours", - "UNMUTED_SUCCESS": "This conversation is unmuted", + "MUTE_CONTACT": "Block Contact", + "UNMUTE_CONTACT": "Unblock Contact", + "MUTED_SUCCESS": "This contact is blocked successfully. You will not be notified of any future conversations.", + "UNMUTED_SUCCESS": "This contact is unblocked successfully.", "SEND_TRANSCRIPT": "Send Transcript", "EDIT_LABEL": "Edit", "SIDEBAR_SECTIONS": { diff --git a/app/models/concerns/conversation_mute_helpers.rb b/app/models/concerns/conversation_mute_helpers.rb index 525346869..c6ea4c7b1 100644 --- a/app/models/concerns/conversation_mute_helpers.rb +++ b/app/models/concerns/conversation_mute_helpers.rb @@ -3,26 +3,16 @@ module ConversationMuteHelpers def mute! resolved! - Redis::Alfred.setex(mute_key, 1, mute_period) + contact.update(blocked: true) create_muted_message end def unmute! - Redis::Alfred.delete(mute_key) + contact.update(blocked: false) create_unmuted_message end def muted? - Redis::Alfred.get(mute_key).present? - end - - private - - def mute_key - format(Redis::RedisKeys::CONVERSATION_MUTE_KEY, id: id) - end - - def mute_period - 6.hours + contact.blocked? end end diff --git a/app/models/contact.rb b/app/models/contact.rb index 23eca55e6..07b29fdf3 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -6,6 +6,7 @@ # # id :integer not null, primary key # additional_attributes :jsonb +# blocked :boolean default(FALSE), not null # contact_type :integer default("visitor") # country_code :string default("") # custom_attributes :jsonb @@ -24,6 +25,7 @@ # Indexes # # index_contacts_on_account_id (account_id) +# index_contacts_on_blocked (blocked) # index_contacts_on_lower_email_account_id (lower((email)::text), account_id) # index_contacts_on_name_email_phone_number_identifier (name,email,phone_number,identifier) USING gin # index_contacts_on_nonempty_fields (account_id,email,phone_number,identifier) WHERE (((email)::text <> ''::text) OR ((phone_number)::text <> ''::text) OR ((identifier)::text <> ''::text)) diff --git a/app/models/conversation.rb b/app/models/conversation.rb index fb2ae8b77..72518f250 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -61,6 +61,7 @@ class Conversation < ApplicationRecord validates :account_id, presence: true validates :inbox_id, presence: true + validates :contact_id, presence: true before_validation :validate_additional_attributes validates :additional_attributes, jsonb_attributes_length: true validates :custom_attributes, jsonb_attributes_length: true @@ -103,7 +104,7 @@ class Conversation < ApplicationRecord has_many :attachments, through: :messages before_save :ensure_snooze_until_reset - before_create :mark_conversation_pending_if_bot + before_create :determine_conversation_status before_create :ensure_waiting_since after_update_commit :execute_after_update_commit_callbacks @@ -226,7 +227,9 @@ class Conversation < ApplicationRecord self.additional_attributes = {} unless additional_attributes.is_a?(Hash) end - def mark_conversation_pending_if_bot + def determine_conversation_status + self.status = :resolved and return if contact.blocked? + # Message template hooks aren't executed for conversations from campaigns # So making these conversations open for agent visibility return if campaign.present? diff --git a/db/migrate/20240213131252_add_blocked_to_contacts.rb b/db/migrate/20240213131252_add_blocked_to_contacts.rb new file mode 100644 index 000000000..6d37484e4 --- /dev/null +++ b/db/migrate/20240213131252_add_blocked_to_contacts.rb @@ -0,0 +1,6 @@ +class AddBlockedToContacts < ActiveRecord::Migration[7.0] + def change + add_column :contacts, :blocked, :boolean, default: false, null: false + add_index :contacts, :blocked + end +end diff --git a/db/schema.rb b/db/schema.rb index 546df444f..775e67f96 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,6 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. + ActiveRecord::Schema[7.0].define(version: 2024_02_16_055809) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" @@ -423,10 +424,12 @@ ActiveRecord::Schema[7.0].define(version: 2024_02_16_055809) do t.string "last_name", default: "" t.string "location", default: "" t.string "country_code", default: "" + t.boolean "blocked", default: false, null: false t.index "lower((email)::text), account_id", name: "index_contacts_on_lower_email_account_id" t.index ["account_id", "email", "phone_number", "identifier"], name: "index_contacts_on_nonempty_fields", where: "(((email)::text <> ''::text) OR ((phone_number)::text <> ''::text) OR ((identifier)::text <> ''::text))" t.index ["account_id"], name: "index_contacts_on_account_id" t.index ["account_id"], name: "index_resolved_contact_account_id", where: "(((email)::text <> ''::text) OR ((phone_number)::text <> ''::text) OR ((identifier)::text <> ''::text))" + t.index ["blocked"], name: "index_contacts_on_blocked" t.index ["email", "account_id"], name: "uniq_email_per_account_contact", unique: true t.index ["identifier", "account_id"], name: "uniq_identifier_per_account_contact", unique: true t.index ["name", "email", "phone_number", "identifier"], name: "index_contacts_on_name_email_phone_number_identifier", opclass: :gin_trgm_ops, using: :gin diff --git a/spec/controllers/api/v1/accounts/contacts_controller_spec.rb b/spec/controllers/api/v1/accounts/contacts_controller_spec.rb index 21511310b..d2527e6a9 100644 --- a/spec/controllers/api/v1/accounts/contacts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/contacts_controller_spec.rb @@ -557,6 +557,27 @@ RSpec.describe 'Contacts API', type: :request do expect(response).to have_http_status(:success) expect(Avatar::AvatarFromUrlJob).to have_been_enqueued.with(contact, 'http://example.com/avatar.png') end + + it 'allows blocking of contact' do + patch "/api/v1/accounts/#{account.id}/contacts/#{contact.id}", + params: { blocked: true }, + headers: admin.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + expect(contact.reload.blocked).to be(true) + end + + it 'allows unblocking of contact' do + contact.update(blocked: true) + patch "/api/v1/accounts/#{account.id}/contacts/#{contact.id}", + params: { blocked: false }, + headers: admin.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + expect(contact.reload.blocked).to be(false) + end end end diff --git a/spec/models/conversation_spec.rb b/spec/models/conversation_spec.rb index efeda965d..2f79b08f2 100644 --- a/spec/models/conversation_spec.rb +++ b/spec/models/conversation_spec.rb @@ -372,9 +372,9 @@ RSpec.describe Conversation do expect(conversation.reload.resolved?).to be(true) end - it 'marks conversation as muted in redis' do + it 'blocks the contact' do mute! - expect(Redis::Alfred.get(conversation.send(:mute_key))).not_to be_nil + expect(conversation.reload.contact.blocked?).to be(true) end it 'creates mute message' do @@ -400,10 +400,9 @@ RSpec.describe Conversation do expect { unmute! }.not_to(change { conversation.reload.status }) end - it 'marks conversation as muted in redis' do - expect { unmute! } - .to change { Redis::Alfred.get(conversation.send(:mute_key)) } - .to nil + it 'unblocks the contact' do + unmute! + expect(conversation.reload.contact.blocked?).to be(false) end it 'creates unmute message' do @@ -549,6 +548,17 @@ RSpec.describe Conversation do end end + describe 'when conversation is created by blocked contact' do + let(:account) { create(:account) } + let(:blocked_contact) { create(:contact, account: account, blocked: true) } + let(:inbox) { create(:inbox, account: account) } + + it 'creates conversation in resolved state' do + conversation = create(:conversation, account: account, contact: blocked_contact, inbox: inbox) + expect(conversation.status).to eq('resolved') + end + end + describe '#botinbox: when conversation created inside inbox with agent bot' do let!(:bot_inbox) { create(:agent_bot_inbox) } let(:conversation) { create(:conversation, inbox: bot_inbox.inbox) } From 27ac262a269b09e0153e5cd23bd8bf34548ac68c Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Thu, 22 Feb 2024 04:15:43 +0530 Subject: [PATCH 15/65] feat(ee): Add support for SLA in automation rules (#8910) Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> --- .../dashboard/helper/automationHelper.js | 9 +++- .../mixins/automations/methodsMixin.js | 12 +++++- .../settings/automation/AddAutomationRule.vue | 17 +++++++- .../automation/EditAutomationRule.vue | 17 +++++++- .../dashboard/settings/automation/Index.vue | 6 +++ .../settings/automation/constants.js | 5 +++ .../routes/dashboard/settings/sla/Index.vue | 16 ++++++- .../shared/mixins/specs/automationFixtures.js | 42 +++++++++++++++++++ .../mixins/specs/automationMixin.spec.js | 7 ++++ 9 files changed, 125 insertions(+), 6 deletions(-) diff --git a/app/javascript/dashboard/helper/automationHelper.js b/app/javascript/dashboard/helper/automationHelper.js index d1a85570b..665e8faf6 100644 --- a/app/javascript/dashboard/helper/automationHelper.js +++ b/app/javascript/dashboard/helper/automationHelper.js @@ -129,7 +129,13 @@ export const agentList = agents => [ ...(agents || []), ]; -export const getActionOptions = ({ agents, teams, labels, type }) => { +export const getActionOptions = ({ + agents, + teams, + labels, + slaPolicies, + type, +}) => { const actionsMap = { assign_agent: agentList(agents), assign_team: teams, @@ -137,6 +143,7 @@ export const getActionOptions = ({ agents, teams, labels, type }) => { add_label: generateConditionOptions(labels, 'title'), remove_label: generateConditionOptions(labels, 'title'), change_priority: PRIORITY_CONDITION_VALUES, + add_sla: slaPolicies, }; return actionsMap[type]; }; diff --git a/app/javascript/dashboard/mixins/automations/methodsMixin.js b/app/javascript/dashboard/mixins/automations/methodsMixin.js index de108910a..a7e8d8120 100644 --- a/app/javascript/dashboard/mixins/automations/methodsMixin.js +++ b/app/javascript/dashboard/mixins/automations/methodsMixin.js @@ -27,6 +27,7 @@ export default { inboxes: 'inboxes/getInboxes', labels: 'labels/getLabels', teams: 'teams/getTeams', + slaPolicies: 'sla/getSLA', }), booleanFilterOptions() { return [ @@ -257,8 +258,15 @@ export default { }; }, getActionDropdownValues(type) { - const { agents, labels, teams } = this; - return getActionOptions({ agents, labels, teams, languages, type }); + const { agents, labels, teams, slaPolicies } = this; + return getActionOptions({ + agents, + labels, + teams, + slaPolicies, + languages, + type, + }); }, manifestCustomAttributes() { const conversationCustomAttributesRaw = this.$store.getters[ diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/AddAutomationRule.vue b/app/javascript/dashboard/routes/dashboard/settings/automation/AddAutomationRule.vue index 67184054a..152b55aae 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/automation/AddAutomationRule.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/automation/AddAutomationRule.vue @@ -144,6 +144,7 @@ diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatMetrics.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatMetrics.vue index d1b9866b0..2c90f987e 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatMetrics.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatMetrics.vue @@ -1,6 +1,6 @@ + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/specs/CsatMetricCard.spec.js b/app/javascript/dashboard/routes/dashboard/settings/reports/components/specs/ReportMetricCard.spec.js similarity index 52% rename from app/javascript/dashboard/routes/dashboard/settings/reports/components/specs/CsatMetricCard.spec.js rename to app/javascript/dashboard/routes/dashboard/settings/reports/components/specs/ReportMetricCard.spec.js index 8c52d2e1a..a2e194333 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/specs/CsatMetricCard.spec.js +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/specs/ReportMetricCard.spec.js @@ -1,46 +1,50 @@ import { createLocalVue, shallowMount } from '@vue/test-utils'; -import CsatMetricCard from '../CsatMetricCard.vue'; +import ReportMetricCard from '../ReportMetricCard.vue'; import VTooltip from 'v-tooltip'; const localVue = createLocalVue(); localVue.use(VTooltip); -describe('CsatMetricCard.vue', () => { +describe('ReportMetricCard.vue', () => { it('renders props correctly', () => { const label = 'Total Responses'; const value = '100'; const infoText = 'Total number of responses'; - const wrapper = shallowMount(CsatMetricCard, { + const wrapper = shallowMount(ReportMetricCard, { propsData: { label, value, infoText }, localVue, stubs: ['fluent-icon'], }); - expect(wrapper.find('.heading span').text()).toMatch(label); - expect(wrapper.find('.metric').text()).toMatch(value); - expect(wrapper.find('.csat--icon').classes()).toContain('has-tooltip'); + expect(wrapper.find({ ref: 'reportMetricLabel' }).text()).toMatch(label); + expect(wrapper.find({ ref: 'reportMetricValue' }).text()).toMatch(value); + expect(wrapper.find({ ref: 'reportMetricInfo' }).classes()).toContain( + 'has-tooltip' + ); }); it('adds disabled class when disabled prop is true', () => { - const wrapper = shallowMount(CsatMetricCard, { + const wrapper = shallowMount(ReportMetricCard, { propsData: { label: '', value: '', infoText: '', disabled: true }, localVue, stubs: ['fluent-icon'], }); - expect(wrapper.find('.csat--metric-card').classes()).toContain('disabled'); + expect(wrapper.classes().join(' ')).toContain( + 'grayscale pointer-events-none opacity-30' + ); }); it('does not add disabled class when disabled prop is false', () => { - const wrapper = shallowMount(CsatMetricCard, { + const wrapper = shallowMount(ReportMetricCard, { propsData: { label: '', value: '', infoText: '', disabled: false }, localVue, stubs: ['fluent-icon'], }); - expect(wrapper.find('.csat--metric-card').classes()).not.toContain( - 'disabled' - ); + expect( + wrapper.find({ ref: 'reportMetricContainer' }).classes().join(' ') + ).not.toContain('grayscale pointer-events-none opacity-30'); }); }); From dca14ef82da5a862d3bd7ad346ebafe6c712d12d Mon Sep 17 00:00:00 2001 From: Pranav Date: Tue, 27 Feb 2024 20:20:59 -0800 Subject: [PATCH 28/65] fix: Downgrade rack-cors to 2.0.0 to fix CVE-2024-27456 (#9032) --- Gemfile | 2 +- Gemfile.lock | 4 ++-- .../dashboard/settings/reports/components/CsatMetrics.vue | 1 + .../settings/reports/components/specs/CSATMetrics.spec.js | 6 ++++-- .../components/specs/__snapshots__/CSATMetrics.spec.js.snap | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index 21e942e0f..59bfc5d7f 100644 --- a/Gemfile +++ b/Gemfile @@ -3,7 +3,7 @@ source 'https://rubygems.org' ruby '3.2.2' ##-- base gems for rails --## -gem 'rack-cors', require: 'rack/cors' +gem 'rack-cors', '2.0.0', require: 'rack/cors' gem 'rails', '~> 7.0.8.1' # Reduces boot times through caching; required in config/boot.rb gem 'bootsnap', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 738a3415f..41cb0f20b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -564,7 +564,7 @@ GEM rack (>= 1.0, < 4) rack-contrib (2.4.0) rack (< 4) - rack-cors (2.0.1) + rack-cors (2.0.0) rack (>= 2.0.0) rack-mini-profiler (3.2.0) rack (>= 1.2.0) @@ -918,7 +918,7 @@ DEPENDENCIES puma pundit rack-attack (>= 6.7.0) - rack-cors + rack-cors (= 2.0.0) rack-mini-profiler (>= 3.2.0) rack-timeout rails (~> 7.0.8.1) diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatMetrics.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatMetrics.vue index 2c90f987e..c9f55bb68 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatMetrics.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatMetrics.vue @@ -21,6 +21,7 @@

{ }); it('hides report card if rating filter is enabled', () => { - expect(wrapper.find('.report-card').exists()).toBe(false); + expect(wrapper.find({ ref: 'csatHorizontalBarChart' }).exists()).toBe( + false + ); }); it('shows report card if rating filter is not enabled', async () => { await wrapper.setProps({ filters: {} }); - expect(wrapper.find('.report-card').exists()).toBe(true); + expect(wrapper.find({ ref: 'csatHorizontalBarChart' }).exists()).toBe(true); }); }); diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/specs/__snapshots__/CSATMetrics.spec.js.snap b/app/javascript/dashboard/routes/dashboard/settings/reports/components/specs/__snapshots__/CSATMetrics.spec.js.snap index 9788a1ef7..6f2a05bf5 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/specs/__snapshots__/CSATMetrics.spec.js.snap +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/specs/__snapshots__/CSATMetrics.spec.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`CsatMetrics.vue computes response count correctly 1`] = ` -
+
From 9f905ce2e628de100777327579bbb52226707feb Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Wed, 28 Feb 2024 12:30:24 +0530 Subject: [PATCH 29/65] feat: Update the input for the SLA threshold selection (#8974) Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Muhsin Keloth Co-authored-by: iamsivin Co-authored-by: Pranav --- .../dashboard/i18n/locale/en/sla.json | 15 +- .../routes/dashboard/settings/sla/Index.vue | 32 ++--- .../routes/dashboard/settings/sla/SlaForm.vue | 136 ++++++++++++------ .../dashboard/settings/sla/SlaTimeInput.vue | 94 ++++++++++++ .../sla/specs/validationMixin.spec.js | 41 ++++++ .../dashboard/settings/sla/validationMixin.js | 11 ++ .../dashboard/settings/sla/validations.js | 11 +- package.json | 2 +- yarn.lock | 8 +- 9 files changed, 278 insertions(+), 72 deletions(-) create mode 100644 app/javascript/dashboard/routes/dashboard/settings/sla/SlaTimeInput.vue diff --git a/app/javascript/dashboard/i18n/locale/en/sla.json b/app/javascript/dashboard/i18n/locale/en/sla.json index 67ec4549f..d87f057eb 100644 --- a/app/javascript/dashboard/i18n/locale/en/sla.json +++ b/app/javascript/dashboard/i18n/locale/en/sla.json @@ -24,21 +24,24 @@ "PLACEHOLDER": "SLA for premium customers" }, "FIRST_RESPONSE_TIME": { - "LABEL": "First Response Time(Seconds)", - "PLACEHOLDER": "300 for 5 minutes" + "LABEL": "First Response Time", + "PLACEHOLDER": "5" }, "NEXT_RESPONSE_TIME": { - "LABEL": "Next Response Time(Seconds)", - "PLACEHOLDER": "600 for 10 minutes" + "LABEL": "Next Response Time", + "PLACEHOLDER": "5" }, "RESOLUTION_TIME": { - "LABEL": "Resolution Time(Seconds)", - "PLACEHOLDER": "86400 for 1 day" + "LABEL": "Resolution Time", + "PLACEHOLDER": "60" }, "BUSINESS_HOURS": { "LABEL": "Business Hours", "PLACEHOLDER": "Only during business hours" }, + "THRESHOLD_TIME": { + "INVALID_FORMAT_ERROR": "Threshold should be a number and greater than zero" + }, "EDIT": "Edit", "CREATE": "Create", "DELETE": "Delete", diff --git a/app/javascript/dashboard/routes/dashboard/settings/sla/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/sla/Index.vue index 6c4002821..9ff510d06 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/sla/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/sla/Index.vue @@ -38,17 +38,17 @@ {{ sla.description }} - {{ sla.first_response_time_threshold }} + {{ displayTime(sla.first_response_time_threshold) }} - {{ sla.next_response_time_threshold }} + {{ displayTime(sla.next_response_time_threshold) }} - {{ sla.resolution_time_threshold }} + {{ displayTime(sla.resolution_time_threshold) }} @@ -88,6 +88,7 @@ diff --git a/app/javascript/dashboard/routes/dashboard/settings/sla/SlaForm.vue b/app/javascript/dashboard/routes/dashboard/settings/sla/SlaForm.vue index 775c0ff2a..f41e8fcfc 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/sla/SlaForm.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/sla/SlaForm.vue @@ -2,44 +2,31 @@
- - - - -
@@ -51,7 +38,7 @@
{{ submitLabel }} @@ -66,10 +53,15 @@ diff --git a/app/javascript/dashboard/routes/dashboard/settings/sla/specs/validationMixin.spec.js b/app/javascript/dashboard/routes/dashboard/settings/sla/specs/validationMixin.spec.js index a828c8556..2266bdccd 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/sla/specs/validationMixin.spec.js +++ b/app/javascript/dashboard/routes/dashboard/settings/sla/specs/validationMixin.spec.js @@ -31,6 +31,7 @@ describe('validationMixin', () => { data() { return { name: '', + thresholdTime: '', }; }, }); @@ -62,4 +63,44 @@ describe('validationMixin', () => { wrapper.vm.$t('SLA.FORM.NAME.MINIMUM_LENGTH_ERROR') ); }); + + it('should accept valid threshold values', () => { + wrapper.setData({ thresholdTime: 10 }); + wrapper.vm.$v.thresholdTime.$touch(); + expect(wrapper.vm.getThresholdTimeErrorMessage).toBe(wrapper.vm.$t('')); + + wrapper.setData({ thresholdTime: 10.5 }); + wrapper.vm.$v.thresholdTime.$touch(); + expect(wrapper.vm.getThresholdTimeErrorMessage).toBe(wrapper.vm.$t('')); + }); + + it('should not return invalid format error message if thresholdTime is empty but not touched', () => { + wrapper.setData({ thresholdTime: '' }); + expect(wrapper.vm.getThresholdTimeErrorMessage).toBe(''); + }); + + it('should return invalid format error message if thresholdTime has an invalid format', () => { + wrapper.setData({ thresholdTime: 'fsdfsdfsdfsd' }); + wrapper.vm.$v.thresholdTime.$touch(); + + expect(wrapper.vm.getThresholdTimeErrorMessage).toBe( + wrapper.vm.$t('SLA.FORM.THRESHOLD_TIME.INVALID_FORMAT_ERROR') + ); + }); + + it('should reject invalid threshold values', () => { + wrapper.setData({ thresholdTime: 0 }); + wrapper.vm.$v.thresholdTime.$touch(); + expect(wrapper.vm.getThresholdTimeErrorMessage).toBe( + wrapper.vm.$t('SLA.FORM.THRESHOLD_TIME.INVALID_FORMAT_ERROR') + ); + }); + + it('should reject invalid threshold values', () => { + wrapper.setData({ thresholdTime: -1 }); + wrapper.vm.$v.thresholdTime.$touch(); + expect(wrapper.vm.getThresholdTimeErrorMessage).toBe( + wrapper.vm.$t('SLA.FORM.THRESHOLD_TIME.INVALID_FORMAT_ERROR') + ); + }); }); diff --git a/app/javascript/dashboard/routes/dashboard/settings/sla/validationMixin.js b/app/javascript/dashboard/routes/dashboard/settings/sla/validationMixin.js index 28f7436c8..d086c6072 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/sla/validationMixin.js +++ b/app/javascript/dashboard/routes/dashboard/settings/sla/validationMixin.js @@ -11,5 +11,16 @@ export default { } return errorMessage; }, + getThresholdTimeErrorMessage() { + let errorMessage = ''; + if (this.$v.thresholdTime.$error) { + if (!this.$v.thresholdTime.numeric || !this.$v.thresholdTime.minValue) { + errorMessage = this.$t( + 'SLA.FORM.THRESHOLD_TIME.INVALID_FORMAT_ERROR' + ); + } + } + return errorMessage; + }, }, }; diff --git a/app/javascript/dashboard/routes/dashboard/settings/sla/validations.js b/app/javascript/dashboard/routes/dashboard/settings/sla/validations.js index bda4b9556..024aa6636 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/sla/validations.js +++ b/app/javascript/dashboard/routes/dashboard/settings/sla/validations.js @@ -1,8 +1,17 @@ -import { required, minLength } from 'vuelidate/lib/validators'; +import { + required, + minLength, + minValue, + decimal, +} from 'vuelidate/lib/validators'; export default { name: { required, minLength: minLength(2), }, + thresholdTime: { + decimal, + minValue: minValue(0.001), + }, }; diff --git a/package.json b/package.json index f7a2f86f7..70ab8a666 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "dependencies": { "@braid/vue-formulate": "^2.5.2", "@chatwoot/prosemirror-schema": "1.0.5", - "@chatwoot/utils": "^0.0.21", + "@chatwoot/utils": "^0.0.23", "@hcaptcha/vue-hcaptcha": "^0.3.2", "@june-so/analytics-next": "^2.0.0", "@radix-ui/colors": "^1.0.1", diff --git a/yarn.lock b/yarn.lock index cc9a6f34c..b28813887 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3177,10 +3177,10 @@ prosemirror-utils "^0.9.6" prosemirror-view "^1.17.2" -"@chatwoot/utils@^0.0.21": - version "0.0.21" - resolved "https://registry.yarnpkg.com/@chatwoot/utils/-/utils-0.0.21.tgz#f9116daac0514a8a8fa6ce594efff10062222be0" - integrity sha512-eUDJ1K5x1rFlBywRctU3hXXiJ1U0EZiklowNl/YJOh1/BWDns4It3DWrQmAcjvsNbEUNWMfY+ShJmjdeei71Cw== +"@chatwoot/utils@^0.0.23": + version "0.0.23" + resolved "https://registry.yarnpkg.com/@chatwoot/utils/-/utils-0.0.23.tgz#e961fd87ef9ee19c442bfcedac5fe0be2ef37726" + integrity sha512-BQ7DprXr7FIkSbHdDc1WonwH0rt/+B+WaaLaXNMjCcxJgkX/gZ8QMltruOfRp/S8cFyd9JfFQhNF0T9lz1OMvA== dependencies: date-fns "^2.29.1" From dafedddc1ac66826c2342116e1495715d967c176 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Wed, 28 Feb 2024 13:56:28 +0530 Subject: [PATCH 30/65] feat: Remove Foundation in favor of Tailwind (#8984) * feat: Remove foundation * chore: Minor fix * Minor fix * Update _forms.scss * chore: More changes * chore: Minor fix * chore: Clean up * fix: font-weight * chore: More changes * chore: Setting page * chore: Editor fix * chore: Reports page * chore: More changes * chore: Minor changes * chore: More fixes * chore: More changes * chore: More changes * chore: More changes * chore: Minor fix * chore: More changes * chore: More changes * chore: More changes * chore: More changes * chore: Clean up * chore: Minor fix * chore: Clean ups * chore: Rename basic file * chore: Remove unused files * chore: Fix expanded input * Fix campaign rendering * chore: Clean up * chore: More changes * chore: Remove unused files * fix: Overflow issue * chore: Minor fix * chore: Clean up * chore: Minor fix * chore: Remove unused files * chore: Minor fix * chore: Minor fix * fix: autoprefixer start/end value has mixed support * chore: Minor fix * chore: Remove unused files * chore: Minor fix * chore: Minor fix * chore: Minor fix * Add responsive design to label settings * fix inbox view * chore: Minor fix * w-60% to w-2/3 * chore: Fix team * chore: Fix button * w-[34%] to w-1/3 * chore: Fix border * Add support mobile views in team page * chore: fix snackbar * chore: clean up * chore: Clean up * fix: loading state alignment * fix: alert styles * chore: Minor fix * fix: spacing for agent bot row * fix: layout * fix: layout for SLA * fix: checkbox * fix: SLA checkbox spacing * Update inbox settings pages * fix macros listing page layout * fix canned responses * chore: Fix bot page * chore: fix automation page * chore: fix agents page * chore: fix canned response editor * chore: Fix settings table * chore: fix settings layout * chore: Minor fix * fix: canned response table layou * fix: layout for table header for webhooks * fix: webhook row layout * fix: dashboard app modal layout * fix: add title to canned response truncated shortcode * fix: dashboard apps row layuot * fix: layouts hooks * fix: body color * fix: delete action color in portal locales * fix: text color for campagin title * fix: success button color --------- Co-authored-by: Pranav Co-authored-by: Vishnu Narayanan Co-authored-by: Shivam Mishra --- .../assets/scss/_foundation-custom.scss | 58 -- .../assets/scss/_foundation-settings.scss | 623 ------------------ .../assets/scss/_helper-classes.scss | 54 +- .../dashboard/assets/scss/_layout.scss | 14 + .../dashboard/assets/scss/_rtl.scss | 50 -- .../dashboard/assets/scss/_typography.scss | 33 - .../assets/scss/_utility-helpers.scss | 73 -- .../dashboard/assets/scss/_woot.scss | 41 +- .../assets/scss/plugins/_dropdown.scss | 43 +- .../assets/scss/plugins/_multiselect.scss | 6 +- .../dashboard/assets/scss/storybook.scss | 9 +- .../assets/scss/views/settings/inbox.scss | 113 +--- .../dashboard/assets/scss/widgets/_base.scss | 140 ++++ .../assets/scss/widgets/_buttons.scss | 89 ++- .../assets/scss/widgets/_conv-header.scss | 1 - .../scss/widgets/_conversation-card.scss | 16 - .../scss/widgets/_conversation-view.scss | 4 +- .../dashboard/assets/scss/widgets/_forms.scss | 78 --- .../dashboard/assets/scss/widgets/_login.scss | 66 -- .../dashboard/assets/scss/widgets/_modal.scss | 75 --- .../assets/scss/widgets/_reply-box.scss | 60 -- .../assets/scss/widgets/_report.scss | 60 -- .../assets/scss/widgets/_reports.scss | 29 - .../assets/scss/widgets/_search-box.scss | 18 - .../assets/scss/widgets/_sidemenu.scss | 78 --- .../assets/scss/widgets/_snackbar.scss | 45 -- .../assets/scss/widgets/_states.scss | 1 - .../assets/scss/widgets/_status-bar.scss | 46 -- .../dashboard/assets/scss/widgets/_tabs.scss | 4 +- .../assets/scss/widgets/_widget_builder.scss | 0 .../assets/scss/widgets/_woot-tables.scss | 4 +- .../components/Accordion/AccordionItem.vue | 2 +- .../dashboard/components/ChannelSelector.vue | 34 +- .../dashboard/components/ChatList.vue | 7 +- .../dashboard/components/CustomAttribute.vue | 18 +- app/javascript/dashboard/components/Modal.vue | 55 +- .../dashboard/components/ModalHeader.vue | 2 +- .../dashboard/components/SettingsSection.vue | 12 +- .../dashboard/components/Snackbar.vue | 14 +- .../components/SnackbarContainer.vue | 6 +- app/javascript/dashboard/components/index.js | 2 - .../layout/config/sidebarItems/campaigns.js | 4 +- .../layout/config/sidebarItems/primaryMenu.js | 2 +- .../sidebarComponents/AccountSelector.vue | 2 +- .../layout/sidebarComponents/AgentDetails.vue | 11 +- .../sidebarComponents/NotificationBell.vue | 4 +- .../SecondaryChildNavItem.vue | 2 +- .../dashboard/components/ui/Label.vue | 17 +- .../dashboard/components/ui/PreviewCard.vue | 34 +- .../dashboard/components/ui/Switch.vue | 2 +- .../dashboard/components/ui/Tabs/TabsItem.vue | 2 +- .../dashboard/components/ui/Wizard.vue | 48 +- .../components/widgets/AttachmentsPreview.vue | 2 +- .../components/widgets/BackButton.vue | 2 +- .../components/widgets/InboxName.vue | 2 +- .../components/widgets/LoadingState.vue | 6 +- .../components/widgets/ReportStatsCard.vue | 57 -- .../components/widgets/SettingIntroBanner.vue | 2 +- .../components/widgets/TableFooter.vue | 4 +- .../components/widgets/Thumbnail.vue | 1 + .../widgets/WootWriter/AudioRecorder.vue | 7 +- .../widgets/WootWriter/ReplyBottomPanel.vue | 2 +- .../widgets/WootWriter/ReplyTopPanel.vue | 2 +- .../widgets/conversation/ConversationCard.vue | 5 +- .../conversation/ConversationHeader.vue | 14 +- .../conversation/EmailTranscriptModal.vue | 9 +- .../widgets/conversation/Message.vue | 4 +- .../widgets/conversation/MessagesView.vue | 2 + .../widgets/conversation/OnboardingView.vue | 12 +- .../widgets/conversation/ReplyBox.vue | 24 +- .../widgets/conversation/ReplyEmailHead.vue | 14 +- .../WhatsappTemplates/TemplatesPicker.vue | 10 +- .../widgets/conversation/bubble/File.vue | 4 +- .../widgets/conversation/bubble/Location.vue | 2 +- .../conversation/bubble/TranslateModal.vue | 4 +- .../conversation/components/GalleryView.vue | 8 +- .../conversationBulkActions/AgentSelector.vue | 14 +- .../conversationBulkActions/Index.vue | 9 +- .../conversationBulkActions/LabelActions.vue | 8 +- .../conversationBulkActions/TeamActions.vue | 22 +- .../conversationBulkActions/UpdateActions.vue | 2 +- .../conversationCardComponents/CardLabels.vue | 15 +- .../components/widgets/forms/Input.vue | 4 - .../components/widgets/forms/PhoneInput.vue | 10 +- .../widgets/modal/ConfirmationModal.vue | 5 - .../components/widgets/modal/DeleteModal.vue | 2 +- .../contact/components/ContactAttribute.vue | 15 +- .../contact/components/ContactFields.vue | 2 +- .../contact/components/ContactIntro.vue | 4 +- .../contact/components/MergeContact.vue | 2 +- .../modules/notes/components/NoteList.vue | 4 +- .../search/components/SearchHeader.vue | 5 +- .../modules/search/components/SearchInput.vue | 4 +- .../components/SearchResultContactItem.vue | 18 +- .../SearchResultConversationItem.vue | 9 +- .../widget-preview/components/WidgetHead.vue | 2 +- .../contacts/components/ContactInfoPanel.vue | 7 +- .../contacts/components/ContactsTable.vue | 2 +- .../contacts/components/SectionHeader.vue | 2 +- .../contacts/components/TimelineCard.vue | 2 +- .../contacts/pages/ContactManageView.vue | 2 +- .../conversation/ConversationParticipant.vue | 2 +- .../conversation/Macros/MacroItem.vue | 3 +- .../conversation/contact/ContactForm.vue | 19 +- .../conversation/contact/ContactInfo.vue | 16 +- .../conversation/contact/ContactInfoRow.vue | 17 +- .../conversation/contact/ConversationForm.vue | 9 +- .../conversation/labels/LabelBox.vue | 1 + .../helpcenter/components/ArticleItem.vue | 6 +- .../ArticleSearch/ArticleSearchResultItem.vue | 2 +- .../components/ArticleSearch/Header.vue | 4 +- .../components/Header/ArticleHeader.vue | 4 +- .../components/PortalListItemTable.vue | 2 +- .../components/PortalSettingsBasicForm.vue | 12 +- .../PortalSettingsCustomizationForm.vue | 12 +- .../helpcenter/components/PortalSwitch.vue | 2 +- .../helpcenter/pages/articles/EditArticle.vue | 2 +- .../pages/categories/NameEmojiInput.vue | 2 +- .../pages/portals/ListAllPortals.vue | 4 +- .../helpcenter/pages/portals/NewPortal.vue | 2 +- .../pages/portals/PortalSettingsFinish.vue | 11 +- .../components/NotificationPanel.vue | 4 +- .../components/NotificationPanelItem.vue | 3 +- .../components/NotificationTable.vue | 8 +- .../components/NotificationsView.vue | 4 +- .../dashboard/settings/SettingsHeader.vue | 8 +- .../settings/SettingsSubPageHeader.vue | 4 +- .../routes/dashboard/settings/Wrapper.vue | 2 +- .../dashboard/settings/account/Index.vue | 6 +- .../dashboard/settings/agentBots/Index.vue | 6 +- .../agentBots/components/AgentBotRow.vue | 13 +- .../agentBots/components/AgentBotType.vue | 2 +- .../dashboard/settings/agents/Index.vue | 5 +- .../settings/attributes/CustomAttribute.vue | 7 +- .../dashboard/settings/attributes/Index.vue | 2 +- .../dashboard/settings/auditlogs/Index.vue | 2 +- .../dashboard/settings/automation/Index.vue | 4 +- .../settings/campaigns/AddCampaign.vue | 4 +- .../settings/campaigns/CampaignCard.vue | 16 +- .../settings/campaigns/campaigns.routes.js | 2 +- .../dashboard/settings/canned/AddCanned.vue | 2 +- .../dashboard/settings/canned/EditCanned.vue | 2 +- .../dashboard/settings/canned/Index.vue | 18 +- .../dashboard/settings/inbox/AddAgents.vue | 6 +- .../dashboard/settings/inbox/ChannelList.vue | 13 +- .../dashboard/settings/inbox/FinishSetup.vue | 8 +- .../dashboard/settings/inbox/ImapSettings.vue | 19 +- .../settings/inbox/InboxChannels.vue | 2 +- .../routes/dashboard/settings/inbox/Index.vue | 9 +- .../settings/inbox/PreChatForm/Settings.vue | 2 +- .../dashboard/settings/inbox/Settings.vue | 52 +- .../dashboard/settings/inbox/SmtpSettings.vue | 23 +- .../settings/inbox/WidgetBuilder.vue | 17 +- .../dashboard/settings/inbox/channels/Api.vue | 4 +- .../settings/inbox/channels/Email.vue | 10 +- .../settings/inbox/channels/Facebook.vue | 19 +- .../settings/inbox/channels/Line.vue | 4 +- .../dashboard/settings/inbox/channels/Sms.vue | 4 +- .../settings/inbox/channels/Telegram.vue | 4 +- .../settings/inbox/channels/Twilio.vue | 4 +- .../settings/inbox/channels/Twitter.vue | 15 +- .../settings/inbox/channels/Website.vue | 4 +- .../settings/inbox/channels/Whatsapp.vue | 4 +- .../emailChannels/ForwardToOption.vue | 4 +- .../channels/emailChannels/Microsoft.vue | 4 +- .../inbox/channels/microsoft/Reauthorize.vue | 4 +- .../inbox/components/BotConfiguration.vue | 4 +- .../settings/inbox/components/BusinessDay.vue | 2 +- .../components/SenderNameExamplePreview.vue | 2 +- .../inbox/components/WeeklyAvailability.vue | 7 +- .../inbox/settingsPage/CollaboratorsPage.vue | 4 +- .../inbox/settingsPage/ConfigurationPage.vue | 40 +- .../integrationapps/IntegrationItem.vue | 10 +- .../MultipleIntegrationHooks.vue | 8 +- .../SingleIntegrationHooks.vue | 6 +- .../DashboardApps/DashboardAppModal.vue | 7 +- .../DashboardApps/DashboardAppsRow.vue | 10 +- .../integrations/DashboardApps/Index.vue | 4 +- .../settings/integrations/Integration.vue | 6 +- .../settings/integrations/Webhooks/Index.vue | 9 +- .../integrations/Webhooks/WebhookForm.vue | 30 +- .../integrations/Webhooks/WebhookRow.vue | 10 +- .../dashboard/settings/labels/AddLabel.vue | 2 +- .../dashboard/settings/labels/EditLabel.vue | 2 +- .../dashboard/settings/labels/Index.vue | 9 +- .../dashboard/settings/macros/Index.vue | 10 +- .../settings/macros/MacroProperties.vue | 3 +- .../settings/profile/ChangePassword.vue | 10 +- .../dashboard/settings/profile/Index.vue | 16 +- .../settings/profile/MessageSignature.vue | 8 +- .../settings/profile/NotificationSettings.vue | 76 +-- .../settings/reports/LiveReports.vue | 16 +- .../reports/components/CsatMetrics.vue | 2 +- .../reports/components/Filters/Labels.vue | 12 +- .../settings/reports/components/Heatmap.vue | 2 +- .../reports/components/ReportFilters.vue | 26 +- .../components/overview/MetricCard.vue | 65 +- .../routes/dashboard/settings/sla/Index.vue | 4 +- .../routes/dashboard/settings/sla/SlaForm.vue | 8 +- .../settings/teams/AgentSelector.vue | 8 +- .../settings/teams/Create/AddAgents.vue | 7 +- .../settings/teams/Create/CreateTeam.vue | 4 +- .../dashboard/settings/teams/Create/Index.vue | 6 +- .../settings/teams/Edit/EditAgents.vue | 9 +- .../settings/teams/Edit/EditTeam.vue | 4 +- .../dashboard/settings/teams/Edit/Index.vue | 6 +- .../dashboard/settings/teams/FinishSetup.vue | 11 +- .../routes/dashboard/settings/teams/Index.vue | 8 +- .../dashboard/settings/teams/TeamForm.vue | 4 +- .../portal/components/SearchSuggestions.vue | 2 +- .../shared/components/GreetingsEditor.vue | 2 +- .../ui/MultiselectDropdownItems.vue | 8 +- app/javascript/shared/mixins/campaignMixin.js | 9 +- .../shared/mixins/specs/campaignMixin.spec.js | 43 +- .../v3/views/auth/password/Edit.vue | 2 +- .../v3/views/auth/reset/password/Index.vue | 2 +- app/views/layouts/vueapp.html.erb | 2 +- package.json | 1 - yarn.lock | 5 - 219 files changed, 1134 insertions(+), 2511 deletions(-) delete mode 100644 app/javascript/dashboard/assets/scss/_foundation-custom.scss delete mode 100644 app/javascript/dashboard/assets/scss/_foundation-settings.scss delete mode 100644 app/javascript/dashboard/assets/scss/_typography.scss delete mode 100644 app/javascript/dashboard/assets/scss/_utility-helpers.scss create mode 100644 app/javascript/dashboard/assets/scss/widgets/_base.scss delete mode 100644 app/javascript/dashboard/assets/scss/widgets/_conv-header.scss delete mode 100644 app/javascript/dashboard/assets/scss/widgets/_conversation-card.scss delete mode 100644 app/javascript/dashboard/assets/scss/widgets/_forms.scss delete mode 100644 app/javascript/dashboard/assets/scss/widgets/_login.scss delete mode 100644 app/javascript/dashboard/assets/scss/widgets/_modal.scss delete mode 100644 app/javascript/dashboard/assets/scss/widgets/_reply-box.scss delete mode 100644 app/javascript/dashboard/assets/scss/widgets/_report.scss delete mode 100644 app/javascript/dashboard/assets/scss/widgets/_reports.scss delete mode 100644 app/javascript/dashboard/assets/scss/widgets/_search-box.scss delete mode 100644 app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss delete mode 100644 app/javascript/dashboard/assets/scss/widgets/_snackbar.scss delete mode 100644 app/javascript/dashboard/assets/scss/widgets/_states.scss delete mode 100644 app/javascript/dashboard/assets/scss/widgets/_status-bar.scss delete mode 100644 app/javascript/dashboard/assets/scss/widgets/_widget_builder.scss delete mode 100644 app/javascript/dashboard/components/widgets/ReportStatsCard.vue diff --git a/app/javascript/dashboard/assets/scss/_foundation-custom.scss b/app/javascript/dashboard/assets/scss/_foundation-custom.scss deleted file mode 100644 index f7d10e633..000000000 --- a/app/javascript/dashboard/assets/scss/_foundation-custom.scss +++ /dev/null @@ -1,58 +0,0 @@ -.button { - font-family: $body-font-family; - font-weight: $font-weight-medium; - - &.round { - border-radius: 1000px; - } -} - -select { - height: 2.5rem; -} - -.card { - margin-bottom: var(--space-small); - padding: var(--space-normal); -} - -code { - border: 0; - font-family: 'ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', - '"Liberation Mono"', '"Courier New"', 'monospace'; - font-size: $font-size-mini; - - &.hljs { - background: $color-background; - border-radius: var(--border-radius-large); - padding: $space-two; - @apply bg-slate-50 dark:bg-slate-700 text-slate-800 dark:text-slate-100; - } -} - -.text-truncate { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.text-capitalize { - text-transform: capitalize; -} - -.cursor-pointer { - cursor: pointer; -} - -// remove when grid gutters are fixed -.columns.with-right-space { - padding-right: var(--space-normal); -} - -.badge { - border-radius: var(--border-radius-normal); -} - -.padding-right-small { - padding-right: var(--space-one); -} diff --git a/app/javascript/dashboard/assets/scss/_foundation-settings.scss b/app/javascript/dashboard/assets/scss/_foundation-settings.scss deleted file mode 100644 index 3fd834cdd..000000000 --- a/app/javascript/dashboard/assets/scss/_foundation-settings.scss +++ /dev/null @@ -1,623 +0,0 @@ -// Foundation for Sites Settings -// ----------------------------- -// -// Table of Contents: -// -// 1. Global -// 2. Breakpoints -// 3. The Grid -// 4. Base Typography -// 5. Typography Helpers -// 6. Abide -// 7. Accordion -// 8. Accordion Menu -// 9. Badge -// 10. Breadcrumbs -// 11. Button -// 12. Button Group -// 13. Callout -// 14. Card -// 15. Close Button -// 16. Drilldown -// 17. Dropdown -// 18. Dropdown Menu -// 19. Forms -// 20. Label -// 21. Media Object -// 22. Menu -// 23. Meter -// 24. Off-canvas -// 25. Orbit -// 26. Pagination -// 27. Progress Bar -// 28. Responsive Embed -// 29. Reveal -// 30. Slider -// 31. Switch -// 32. Table -// 33. Tabs -// 34. Thumbnail -// 35. Title Bar -// 36. Tooltip -// 37. Top Bar - -@import '~foundation-sites/scss/util/util'; -// 1. Global -// --------- - -// Disable contrast warnings in Foundation. -$contrast-warnings: false; - -$global-font-size: 16px; -$global-width: 100%; -$global-lineheight: 1.5; -$foundation-palette: (primary: $color-woot, - secondary: #5d7592, - success: #44ce4b, - warning: #ffc532, - alert: #ff382d); -$light-gray: #c0ccda; -$medium-gray: #8492a6; -$dark-gray: $color-gray; -$black: #000; -$white: #fff; -$body-background: $white; -$body-font-color: $color-body; -$body-font-family: 'PlusJakarta', --apple-system, -system-ui, -BlinkMacSystemFont, -"Segoe UI", -Roboto, -"Helvetica Neue", -Tahoma, -Arial, -sans-serif; -$body-antialiased: true; -$global-margin: $space-small; -$global-padding: $space-small; -$global-weight-normal: normal; -$global-weight-bold: bold; -$global-radius: 0; -$global-text-direction: ltr; -$global-flexbox: false; -$print-transparent-backgrounds: true; - -@include add-foundation-colors; - -// 2. Breakpoints -// -------------- - -$breakpoints: (small: 0, - medium: 640px, - large: 1024px, - xlarge: 1200px, - xxlarge: 1400px, - xxxlarge: 1600px, -); -$print-breakpoint: large; -$breakpoint-classes: (small medium large); - -// 3. The Grid -// ----------- - -$grid-row-width: $global-width; -$grid-column-count: 12; -$grid-column-gutter: (small: $zero, - medium: $zero); -$grid-column-align-edge: true; -$block-grid-max: 8; - -// 4. Base Typography -// ------------------ - -$header-font-family: $body-font-family; -$header-font-weight: $font-weight-medium; -$header-font-style: normal; -$font-family-monospace: $body-font-family; -$header-color: $color-heading; -$header-lineheight: 1.4; -$header-margin-bottom: 0.3125rem; -$header-styles: (small: ("h1": ("font-size": 24), - "h2": ("font-size": 20), - "h3": ("font-size": 19), - "h4": ("font-size": 18), - "h5": ("font-size": 17), - "h6": ("font-size": 16)), - medium: ("h1": ("font-size": 48), - "h2": ("font-size": 40), - "h3": ("font-size": 31), - "h4": ("font-size": 25), - "h5": ("font-size": 20), - "h6": ("font-size": 16))); -$header-text-rendering: optimizeLegibility; -$small-font-size: 80%; -$header-small-font-color: $medium-gray; -$paragraph-lineheight: 1.65; -$paragraph-margin-bottom: var(--space-small); -$paragraph-text-rendering: optimizeLegibility; -$code-color: $black; -$code-font-family: $font-family-monospace; -$code-font-weight: $global-weight-normal; -$code-background: $light-gray; -$code-border: 1px solid $medium-gray; -$code-padding: rem-calc(2 5 1); -$anchor-color: $primary-color; -$anchor-color-hover: scale-color($anchor-color, $lightness: -14%); -$anchor-text-decoration: none; -$anchor-text-decoration-hover: none; -$hr-width: $global-width; -$hr-border: 1px solid $medium-gray; -$hr-margin: rem-calc(20) auto; -$list-lineheight: $paragraph-lineheight; -$list-margin-bottom: $paragraph-margin-bottom; -$list-style-type: disc; -$list-style-position: outside; -$list-side-margin: 0.78125rem; -$list-nested-side-margin: 0.78125rem; -$defnlist-margin-bottom: 0.6875rem; -$defnlist-term-weight: $global-weight-bold; -$defnlist-term-margin-bottom: 0.1875rem; -$blockquote-color: $dark-gray; -$blockquote-padding: rem-calc(9 20 0 19); -$blockquote-border: 1px solid $medium-gray; -$cite-font-size: rem-calc(13); -$cite-color: $dark-gray; -$cite-pseudo-content: '\2014 \0020'; -$keystroke-font: $font-family-monospace; -$keystroke-color: $black; -$keystroke-background: $light-gray; -$keystroke-padding: rem-calc(2 4 0); -$keystroke-radius: $global-radius; -$abbr-underline: 1px dotted $black; - -// 5. Typography Helpers -// --------------------- - -$lead-font-size: $global-font-size * 1.25; -$lead-lineheight: 1.6; -$subheader-lineheight: 1.4; -$subheader-color: $dark-gray; -$subheader-font-weight: $global-weight-normal; -$subheader-margin-top: 0.125rem; -$subheader-margin-bottom: 0.3125rem; -$stat-font-size: 1.5625rem; - -// 6. Abide -// -------- - -$abide-inputs: true; -$abide-labels: true; -$input-background-invalid: get-color(alert); -$form-label-color-invalid: get-color(alert); -$input-error-color: get-color(alert); -$input-error-font-size: rem-calc(12); -$input-error-font-weight: $global-weight-bold; - -// 7. Accordion -// ------------ - -$accordion-background: $white; -$accordion-plusminus: true; -$accordion-title-font-size: rem-calc(12); -$accordion-item-color: $primary-color; -$accordion-item-background-hover: $light-gray; -$accordion-item-padding: 0.78125rem 0.625rem; -$accordion-content-background: $white; -$accordion-content-border: 1px solid $light-gray; -$accordion-content-color: $body-font-color; -$accordion-content-padding: 0.625rem; - -// 8. Accordion Menu -// ----------------- - -$accordionmenu-arrows: true; -$accordionmenu-arrow-color: $primary-color; -$accordionmenu-arrow-size: 6px; - -// 9. Badge -// -------- - -$badge-background: $primary-color; -$badge-color: $white; -$badge-color-alt: $black; -$badge-palette: $foundation-palette; -$badge-padding: var(--space-smaller); -$badge-minwidth: 2.1em; -$badge-font-size: var(--font-size-nano); - -// 10. Breadcrumbs -// --------------- - -$breadcrumbs-margin: 0 0 $global-margin 0; -$breadcrumbs-item-font-size: rem-calc(11); -$breadcrumbs-item-color: $primary-color; -$breadcrumbs-item-color-current: $black; -$breadcrumbs-item-color-disabled: $medium-gray; -$breadcrumbs-item-margin: 0.46875rem; -$breadcrumbs-item-uppercase: true; -$breadcrumbs-item-slash: true; - -// 11. Button -// ---------- - -$button-padding: var(--space-smaller) 1em; -$button-margin: 0 0 $global-margin 0; -$button-fill: solid; -$button-background: $primary-color; -$button-background-hover: scale-color($button-background, $lightness: -15%); -$button-color: $white; -$button-color-alt: $white; -$button-radius: var(--border-radius-normal); -$button-sizes: (tiny: var(--font-size-micro), - small: var(--font-size-mini), - default: var(--font-size-small), - large: var(--font-size-medium)); -$button-palette: $foundation-palette; -$button-opacity-disabled: 0.4; -$button-background-hover-lightness: -20%; -$button-hollow-hover-lightness: -50%; -$button-transition: background-color 0.25s ease-out, -color 0.25s ease-out; - -// 12. Button Group -// ---------------- - -$buttongroup-margin: 0; -$buttongroup-spacing: 0; -$buttongroup-child-selector: '.button'; -$buttongroup-expand-max: 6; -$buttongroup-radius-on-each: false; - -// 13. Callout -// ----------- - -$callout-background: $white; -$callout-background-fade: 85%; -$callout-border: 1px solid rgba($black, 0.25); -$callout-margin: 0 0 0.625rem 0; -$callout-padding: 0.625rem; -$callout-font-color: $body-font-color; -$callout-font-color-alt: $body-background; -$callout-radius: $global-radius; -$callout-link-tint: 30%; - -// 14. Card -// -------- - -$card-background: $white; -$card-font-color: $body-font-color; -$card-divider-background: $light-gray; -$card-border: 1px solid var(--color-border); -$card-shadow: var(--shadow-small); -$card-border-radius: var(--border-radius-normal); -$card-padding: var(--space-small); -$card-margin: $global-margin; - -// 15. Close Button -// ---------------- - -$closebutton-position: right top; -$closebutton-offset-horizontal: (small: 0.66rem, - medium: 1rem); -$closebutton-offset-vertical: (small: 0.33em, - medium: 0.5rem); -$closebutton-size: (small: 1.5em, - medium: 2em); -$closebutton-lineheight: 1; -$closebutton-color: $dark-gray; -$closebutton-color-hover: $black; - -// 16. Drilldown -// ------------- - -$drilldown-transition: transform 0.15s linear; -$drilldown-arrows: true; -$drilldown-arrow-color: $primary-color; -$drilldown-arrow-size: 6px; -$drilldown-background: $white; - -// 17. Dropdown -// ------------ - -$dropdown-padding: 0.625rem; -$dropdown-background: $body-background; -$dropdown-border: 1px solid $medium-gray; -$dropdown-font-size: 0.625rem; -$dropdown-width: 300px; -$dropdown-radius: $global-radius; -$dropdown-sizes: (tiny: 100px, - small: 200px, - large: 400px); - -// 18. Dropdown Menu -// ----------------- - -$dropdownmenu-arrows: true; -$dropdownmenu-arrow-color: $anchor-color; -$dropdownmenu-arrow-size: 6px; -$dropdownmenu-min-width: 200px; -$dropdownmenu-background: $white; -$dropdownmenu-border: 1px solid $medium-gray; - -// 19. Forms -// --------- - -$fieldset-border: 1px solid $light-gray; -$fieldset-padding: $space-two; -$fieldset-margin: $space-one $zero; -$legend-padding: rem-calc(0 3); -$form-spacing: $space-normal; -$helptext-color: $color-body; -$helptext-font-size: $font-size-small; -$helptext-font-style: italic; -$input-prefix-color: $color-body; -$input-prefix-background: var(--b-100); -$input-prefix-border: 1px solid $color-border; -$input-prefix-padding: 0.625rem; -$form-label-color: $color-body; -$form-label-font-size: rem-calc(14); -$form-label-font-weight: $font-weight-medium; -$form-label-line-height: 1.8; -$select-background: $white; -$select-triangle-color: $dark-gray; -$select-radius: var(--border-radius-normal); -$input-color: $color-body; -$input-placeholder-color: $light-gray; -$input-font-family: inherit; -$input-font-size: $font-size-default; -$input-font-weight: $global-weight-normal; -$input-background: $white; -$input-background-focus: $white; -$input-background-disabled: $light-gray; -$input-border: 1px solid var(--s-200); -$input-border-focus: 1px solid lighten($primary-color, 15%); -$input-shadow: 0; -$input-shadow-focus: 0; -$input-cursor-disabled: not-allowed; -$input-transition: border-color 0.25s ease-in-out; -$input-number-spinners: true; -$input-radius: var(--border-radius-normal); -$form-button-radius: var(--border-radius-normal); - -// 20. Label -// --------- - -$label-background: $white; -$label-color: $black; -$label-color-alt: $black; -$label-palette: $foundation-palette; -$label-font-size: $font-size-mini; -$label-padding: $space-smaller $space-small; -$label-radius: var(--border-radius-small); - -// 21. Media Object -// ---------------- - -$mediaobject-margin-bottom: $global-margin; -$mediaobject-section-padding: $global-padding; -$mediaobject-image-width-stacked: 100%; - -// 22. Menu -// -------- - -$menu-margin: 0; -$menu-margin-nested: $space-medium; -$menu-item-padding: $space-slab; -$menu-item-color-active: $white; -$menu-item-background-active: $color-background; -$menu-icon-spacing: 0.15625rem; -$menu-item-background-hover: $light-gray; -$menu-border: $light-gray; - -// 23. Meter -// --------- - -$meter-height: 0.625rem; -$meter-radius: $global-radius; -$meter-background: $medium-gray; -$meter-fill-good: $success-color; -$meter-fill-medium: $warning-color; -$meter-fill-bad: $alert-color; - -// 24. Off-canvas -// -------------- - -$offcanvas-sizes: (small: 14.375, - medium: 14.375, -); -$offcanvas-vertical-sizes: (small: 14.375, - medium: 14.375, -); -$offcanvas-background: $light-gray; -$offcanvas-shadow: 0 0 10px rgba($black, 0.7); -$offcanvas-push-zindex: 1; -$offcanvas-overlap-zindex: 10; -$offcanvas-reveal-zindex: 1; -$offcanvas-transition-length: 0.5s; -$offcanvas-transition-timing: ease; -$offcanvas-fixed-reveal: true; -$offcanvas-exit-background: rgba($white, 0.25); -$maincontent-class: 'off-canvas-content'; - -// 25. Orbit -// --------- - -$orbit-bullet-background: $medium-gray; -$orbit-bullet-background-active: $dark-gray; -$orbit-bullet-diameter: 0.75rem; -$orbit-bullet-margin: 0.0625rem; -$orbit-bullet-margin-top: 0.5rem; -$orbit-bullet-margin-bottom: 0.5rem; -$orbit-caption-background: rgba($black, 0.5); -$orbit-caption-padding: 0.625rem; -$orbit-control-background-hover: rgba($black, 0.5); -$orbit-control-padding: 0.625rem; -$orbit-control-zindex: 10; - -// 26. Pagination -// -------------- - -$pagination-font-size: rem-calc(14); -$pagination-margin-bottom: $global-margin; -$pagination-item-color: $black; -$pagination-item-padding: rem-calc(3 10); -$pagination-item-spacing: rem-calc(1); -$pagination-radius: $global-radius; -$pagination-item-background-hover: $light-gray; -$pagination-item-background-current: $primary-color; -$pagination-item-color-current: $white; -$pagination-item-color-disabled: $medium-gray; -$pagination-ellipsis-color: $black; -$pagination-mobile-items: false; -$pagination-mobile-current-item: false; -$pagination-arrows: true; - -// 27. Progress Bar -// ---------------- - -$progress-height: 0.625rem; -$progress-background: $medium-gray; -$progress-margin-bottom: $global-margin; -$progress-meter-background: $primary-color; -$progress-radius: $global-radius; - -// 28. Responsive Embed -// -------------------- - -$responsive-embed-margin-bottom: rem-calc(16); -$responsive-embed-ratios: (default: 4 by 3, - widescreen: 16 by 9); - -// 29. Reveal -// ---------- - -$reveal-background: $white; -$reveal-width: 600px; -$reveal-max-width: $global-width; -$reveal-padding: $global-padding; -$reveal-border: 1px solid $medium-gray; -$reveal-radius: $global-radius; -$reveal-zindex: 1005; -$reveal-overlay-background: rgba($black, 0.45); - -// 30. Slider -// ---------- - -$slider-width-vertical: 0.3125rem; -$slider-transition: all 0.2s ease-in-out; -$slider-height: 0.3125rem; -$slider-background: $light-gray; -$slider-fill-background: $medium-gray; -$slider-handle-height: 0.875rem; -$slider-handle-width: 0.875rem; -$slider-handle-background: $primary-color; -$slider-opacity-disabled: 0.25; -$slider-radius: $global-radius; - -// 31. Switch -// ---------- - -$switch-background: $light-gray; -$switch-background-active: $primary-color; -$switch-height: $space-two; -$switch-height-tiny: $space-slab; -$switch-height-small: $space-normal; -$switch-height-large: $space-large; -$switch-radius: $space-large; -$switch-margin: $global-margin; -$switch-paddle-background: $white; -$switch-paddle-offset: $space-micro; -$switch-paddle-radius: $space-large; -$switch-paddle-transition: all 0.15s ease-out; - -// 32. Table -// --------- - -$table-background: transparent; -$table-color-scale: 5%; -$table-border: 1px solid transparent; -$table-padding: rem-calc(8 10 10); -$table-hover-scale: 2%; -$table-row-hover: darken($table-background, $table-hover-scale); -$table-row-stripe-hover: darken($table-background, - $table-color-scale + $table-hover-scale); -$table-is-striped: false; -$table-striped-background: smart-scale($table-background, $table-color-scale); -$table-stripe: even; -$table-head-background: smart-scale($table-background, $table-color-scale / 2); -$table-head-row-hover: darken($table-head-background, $table-hover-scale); -$table-foot-background: smart-scale($table-background, $table-color-scale); -$table-foot-row-hover: darken($table-foot-background, $table-hover-scale); -$table-head-font-color: $body-font-color; -$table-foot-font-color: $body-font-color; -$show-header-for-stacked: false; - -// 33. Tabs -// -------- - -$tab-margin: 0; - -$tab-background: transparent; -$tab-background-active: transparent; -$tab-item-font-size: $font-size-small; -$tab-item-background-hover: transparent; -$tab-item-padding: $space-one $zero; -$tab-color: $primary-color; -$tab-active-color: $primary-color; -$tab-expand-max: 6; -$tab-content-background: transparent; -$tab-content-border: transparent; -$tab-content-color: foreground($tab-background, $primary-color); -$tab-content-padding: 0.625rem; - -// 34. Thumbnail -// ------------- - -$thumbnail-border: solid 4px $white; -$thumbnail-margin-bottom: $global-margin; -$thumbnail-shadow: 0 0 0 1px rgba($black, 0.2); -$thumbnail-shadow-hover: 0 0 6px 1px rgba($primary-color, 0.5); -$thumbnail-transition: box-shadow 200ms ease-out; -$thumbnail-radius: $global-radius; - -// 35. Title Bar -// ------------- - -$titlebar-background: $black; -$titlebar-color: $white; -$titlebar-padding: 0.3125rem; -$titlebar-text-font-weight: bold; -$titlebar-icon-color: $white; -$titlebar-icon-color-hover: $medium-gray; -$titlebar-icon-spacing: 0.15625rem; - -// 36. Tooltip -// ----------- - -$has-tip-font-weight: $global-weight-bold; -$has-tip-border-bottom: dotted 1px $dark-gray; -$tooltip-background-color: $black; -$tooltip-color: $white; -$tooltip-padding: 0.46875rem; -$tooltip-font-size: $font-size-mini; -$tooltip-pip-width: 0.46875rem; -$tooltip-pip-height: $tooltip-pip-width * 0.866; -$tooltip-radius: $global-radius; - -// 37. Top Bar -// ----------- - -$topbar-padding: 0.3125; -$topbar-background: $light-gray; -$topbar-submenu-background: $topbar-background; -$topbar-title-spacing: 0.3125 0.625rem 0.3125 0; -$topbar-input-width: 200px; -$topbar-unstack-breakpoint: medium; - - -// Internal variable that contains the flex justifying options -$-zf-flex-justify: -zf-flex-justify($global-text-direction); - -$menu-items-padding: $space-one; -$xy-grid: false; diff --git a/app/javascript/dashboard/assets/scss/_helper-classes.scss b/app/javascript/dashboard/assets/scss/_helper-classes.scss index 0b400c4e9..48ee1918b 100644 --- a/app/javascript/dashboard/assets/scss/_helper-classes.scss +++ b/app/javascript/dashboard/assets/scss/_helper-classes.scss @@ -1,64 +1,22 @@ -.bg-light { - @apply bg-slate-25 dark:bg-slate-800; -} - -.flex-center { - @include flex-align(center, middle); - display: flex; -} - -.bottom-space-fix { - margin-bottom: auto; -} - -.full-height { - @include full-height(); -} - +// loader class .spinner { @include color-spinner(); - display: inline-block; - height: $space-medium; - padding: $zero $space-medium; - position: relative; - vertical-align: middle; - width: $space-medium; + @apply inline-block h-6 py-0 px-6 relative align-middle w-6; &.message { @include normal-shadow; - background: $color-white; - border-radius: $space-large; - left: 0; - margin: $space-slab auto; - padding: $space-normal; - top: 0; + @apply bg-white dark:bg-slate-800 rounded-full left-0 my-3 mx-auto p-4 top-0; &::before { - margin-left: -$space-slab; - margin-top: -$space-slab; + @apply -ml-3 -mt-3; } } &.small { - height: $space-normal; - width: $space-normal; + @apply h-4 w-4; &::before { - height: $space-normal; - margin-top: -$space-small; - width: $space-normal; + @apply h-4 -mt-2 w-4; } } } - -.justify-space-between { - justify-content: space-between; -} - -.w-full { - width: 100%; -} - -.h-full { - height: 100%; -} diff --git a/app/javascript/dashboard/assets/scss/_layout.scss b/app/javascript/dashboard/assets/scss/_layout.scss index 594a14d95..ea40c1f3a 100644 --- a/app/javascript/dashboard/assets/scss/_layout.scss +++ b/app/javascript/dashboard/assets/scss/_layout.scss @@ -1,5 +1,19 @@ +// scss-lint:disable SpaceAfterPropertyColon +// @import 'shared/assets/fonts/inter'; + html, body { + font-family: + 'PlusJakarta', + Inter, + -apple-system, + system-ui, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + 'Helvetica Neue', + Arial, + sans-serif !important; -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; height: 100%; diff --git a/app/javascript/dashboard/assets/scss/_rtl.scss b/app/javascript/dashboard/assets/scss/_rtl.scss index 6f16a2af4..36679fc60 100644 --- a/app/javascript/dashboard/assets/scss/_rtl.scss +++ b/app/javascript/dashboard/assets/scss/_rtl.scss @@ -131,37 +131,12 @@ } } - .search-header--wrap { - .search--input { - text-align: right; - } - - .layout-switch__container { - transform: rotate(180deg); - } - } - // Basic filter dropdown .basic-filter { left: 0; right: unset; } - // Card label - .label-container { - .label { - margin-left: var(--space-smaller); - margin-right: 0; - } - } - - // Secondary sidebar toggle button - .toggle-sidebar { - margin-left: 0; - margin-right: var(--space-minus-small); - transform: rotate(180deg); - } - // Bulk actions .bulk-action__container { .triangle { @@ -202,22 +177,6 @@ } } - // Notification panel - .notification-wrap { - left: 0; - right: var(--space-jumbo); - - .action-button { - margin-left: var(--space-small); - margin-right: 0; - } - - .notification-content--wrap { - margin-left: 0; - margin-right: var(--space-small); - } - } - // Help center .article-container .row--article-block { td:last-child { @@ -324,10 +283,6 @@ // Other changes - .account-selector--wrap { - direction: initial; - } - .colorpicker--chrome { direction: initial; } @@ -347,9 +302,4 @@ .contact--form .input-group { direction: initial; } - - // scss-lint:disable QualifyingElement - .dropdown-menu--header > span.title { - text-align: right; - } } diff --git a/app/javascript/dashboard/assets/scss/_typography.scss b/app/javascript/dashboard/assets/scss/_typography.scss deleted file mode 100644 index ee35a9cf0..000000000 --- a/app/javascript/dashboard/assets/scss/_typography.scss +++ /dev/null @@ -1,33 +0,0 @@ -.page-title { - font-size: $font-size-big; -} - -.page-sub-title { - font-size: $font-size-large; - word-wrap: break-word; -} - -.block-title { - font-size: $font-size-medium; -} - -.sub-block-title { - font-size: $font-size-default; -} - -.text-block-title { - font-size: $font-size-small; -} - -.text-muted { - color: var(--s-300); -} - -a { - font-size: $font-size-small; -} - -p { - font-size: $font-size-small; - word-spacing: .12em; -} diff --git a/app/javascript/dashboard/assets/scss/_utility-helpers.scss b/app/javascript/dashboard/assets/scss/_utility-helpers.scss deleted file mode 100644 index 99f0fe3eb..000000000 --- a/app/javascript/dashboard/assets/scss/_utility-helpers.scss +++ /dev/null @@ -1,73 +0,0 @@ -.margin-bottom-small { - margin-bottom: var(--space-small); -} - -.margin-right-smaller { - margin-right: var(--space-smaller); -} - -.margin-left-minus-slab { - margin-left: var(--space-minus-slab); -} - -.margin-right-minus-slab { - margin-right: var(--space-minus-slab); -} - -.fs-small { - font-size: var(--font-size-small); -} - -.fs-default { - font-size: var(--font-size-default); -} - -.fw-medium { - font-weight: var(--font-weight-medium); -} - -.p-normal { - padding: var(--space-normal); -} - -.overflow-scroll { - overflow: scroll; -} - -.overflow-auto { - overflow: auto; -} - -.overflow-hidden { - overflow: hidden; -} - -.border-right { - @apply border-r border-slate-50 dark:border-slate-700; -} - -.border-left { - border-left: 1px solid var(--color-border); -} - -.text-ellipsis { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.flex-between { - align-items: center; - display: flex; - justify-content: space-between; -} - -.flex-end { - display: flex; - justify-content: end; -} - -.flex-align-center { - align-items: center; - display: flex; -} diff --git a/app/javascript/dashboard/assets/scss/_woot.scss b/app/javascript/dashboard/assets/scss/_woot.scss index 2773ced6f..cdc522224 100644 --- a/app/javascript/dashboard/assets/scss/_woot.scss +++ b/app/javascript/dashboard/assets/scss/_woot.scss @@ -15,62 +15,23 @@ @import 'variables'; @import 'mixins'; -@import 'foundation-settings'; @import 'helper-classes'; @import 'formulate'; @import 'date-picker'; -@import 'foundation-sites/scss/foundation'; - -@include foundation-everything($flex: true); - -@include foundation-prototype-text-utilities; -@include foundation-prototype-text-transformation; -@include foundation-prototype-text-decoration; -@include foundation-prototype-font-styling; -@include foundation-prototype-list-style-type; -@include foundation-prototype-rounded; -@include foundation-prototype-bordered; -@include foundation-prototype-shadow; -@include foundation-prototype-separator; -@include foundation-prototype-overflow; -@include foundation-prototype-display; -@include foundation-prototype-position; -@include foundation-prototype-border-box; -@include foundation-prototype-border-none; -@include foundation-prototype-sizing; -@include foundation-prototype-spacing; - -@import 'typography'; @import 'layout'; @import 'animations'; -@import 'foundation-custom'; @import 'rtl'; +@import 'widgets/base'; @import 'widgets/buttons'; -@import 'widgets/conv-header'; -@import 'widgets/conversation-card'; @import 'widgets/conversation-view'; -@import 'widgets/forms'; -@import 'widgets/login'; -@import 'widgets/modal'; -@import 'widgets/reply-box'; -@import 'widgets/report'; -@import 'widgets/search-box'; -@import 'widgets/sidemenu'; -@import 'widgets/snackbar'; -@import 'widgets/states'; -@import 'widgets/status-bar'; @import 'widgets/tabs'; @import 'widgets/woot-tables'; -@import 'views/settings/inbox'; -@import 'views/settings/integrations'; - @import 'plugins/multiselect'; @import 'plugins/dropdown'; @import '~shared/assets/stylesheets/ionicons'; -@import 'utility-helpers'; .tooltip { @apply bg-slate-900 text-white py-1 px-2 z-40 text-xs rounded-md dark:bg-slate-200 dark:text-slate-900; diff --git a/app/javascript/dashboard/assets/scss/plugins/_dropdown.scss b/app/javascript/dashboard/assets/scss/plugins/_dropdown.scss index 100662394..e08099ae6 100644 --- a/app/javascript/dashboard/assets/scss/plugins/_dropdown.scss +++ b/app/javascript/dashboard/assets/scss/plugins/_dropdown.scss @@ -1,46 +1,7 @@ .dropdown-pane { - @include elegant-card; - @include border-light; - box-sizing: content-box; - padding: var(--space-small); - width: fit-content; - z-index: var(--z-index-very-high); + @apply border rounded-lg hidden relative invisible shadow-lg border-slate-25 dark:border-slate-700 box-content p-2 w-fit z-[9999]; &.dropdown-pane--open { - @apply bg-white dark:bg-slate-800; - display: block; - visibility: visible; - } - - &.dropdowm--bottom { - &::before { - @include arrow(top, var(--color-border-light), 14px); - position: absolute; - right: 6px; - top: -14px; - } - - &::after { - @include arrow(top, $color-white, var(--space-slab)); - position: absolute; - right: var(--space-small); - top: -12px; - } - } - - &.dropdowm--top { - &::before { - @include arrow(bottom, var(--color-border-light), 14px); - bottom: -14px; - position: absolute; - right: 6px; - } - - &::after { - @include arrow(bottom, $color-white, var(--space-slab)); - bottom: -12px; - position: absolute; - right: var(--space-small); - } + @apply bg-white absolute dark:bg-slate-800 block visible; } } diff --git a/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss b/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss index 0fddae3d1..9170715e0 100644 --- a/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss +++ b/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss @@ -97,6 +97,10 @@ .multiselect__tags { @apply bg-white dark:bg-slate-900 border border-solid border-slate-200 dark:border-slate-600 m-0 min-h-[2.875rem] pt-0; + + input { + @apply border-0 border-none; + } } .multiselect__tags-wrap { @@ -149,7 +153,6 @@ } .multiselect-wrap--small { - .multiselect__tags, .multiselect__input, .multiselect { @@ -180,7 +183,6 @@ .multiselect--disabled .multiselect__select { @apply bg-transparent; } - } .multiselect-wrap--medium { diff --git a/app/javascript/dashboard/assets/scss/storybook.scss b/app/javascript/dashboard/assets/scss/storybook.scss index 457560aa9..fa2455918 100644 --- a/app/javascript/dashboard/assets/scss/storybook.scss +++ b/app/javascript/dashboard/assets/scss/storybook.scss @@ -13,20 +13,14 @@ @import '~shared/assets/stylesheets/ionicons'; @import 'mixins'; -@import 'foundation-settings'; @import 'helper-classes'; -@import 'foundation-sites/scss/foundation'; - -@include foundation-prototype-spacing; -@include foundation-everything($flex: true); @import 'typography'; @import 'layout'; @import 'animations'; -@import 'foundation-custom'; @import 'widgets/buttons'; -@import 'widgets/forms'; +@import 'widgets/base'; @import 'plugins/multiselect'; @@ -36,7 +30,6 @@ @import 'tailwindcss/utilities'; @import 'widget/assets/scss/utilities'; - html, body { font-family: 'PlusJakarta', sans-serif; diff --git a/app/javascript/dashboard/assets/scss/views/settings/inbox.scss b/app/javascript/dashboard/assets/scss/views/settings/inbox.scss index 39e4ed2bd..8a118b906 100644 --- a/app/javascript/dashboard/assets/scss/views/settings/inbox.scss +++ b/app/javascript/dashboard/assets/scss/views/settings/inbox.scss @@ -1,112 +1 @@ -.settings { - @apply overflow-auto; -} - -.wizard-box { - .item { - @apply cursor-pointer py-4 pr-4 pl-6 relative; - - &::before, - &::after { - @apply bg-slate-75 dark:bg-slate-600 content-[''] h-full absolute top-5 w-0.5; - } - - &::before { - @apply h-4 top-0; - } - - &:first-child { - &::before { - @apply h-0; - } - } - - &:last-child { - &::after { - @apply h-0; - } - } - - &.active { - h3 { - @apply text-woot-500 dark:text-woot-500; - } - - .step { - @apply bg-woot-500 dark:bg-woot-500; - } - } - - &.over { - &::after { - @apply bg-woot-500 dark:bg-woot-500; - } - - .step { - @apply bg-woot-500 dark:bg-woot-500; - } - - & + .item { - &::before { - @apply bg-woot-500 dark:bg-woot-500; - } - } - } - - h3 { - @apply text-slate-800 dark:text-slate-100 text-base pl-6; - } - - .completed { - @apply text-green-500 dark:text-green-500 ml-1; - } - - p { - @apply text-slate-600 dark:text-slate-300 text-sm m-0 pl-6; - } - - .step { - @apply bg-slate-75 dark:bg-slate-600 rounded-2xl font-medium w-4 left-4 leading-4 z-[999] absolute text-center text-white dark:text-white text-xxs top-5; - - i { - @apply text-xxs; - } - } - } -} - -.wizard-body { - @apply border border-slate-25 dark:border-slate-800/60 bg-white dark:bg-slate-900 h-full p-6; - - &.height-auto { - @apply h-auto; - } -} - -.settings--content { - @apply my-2 mx-8; - - .title { - @apply font-medium; - } - - .code { - @apply bg-slate-50 dark:bg-slate-800 overflow-auto p-2.5 whitespace-nowrap; - - code { - @apply bg-transparent border-0; - } - } -} - -.login-init { - @apply pt-[30%] text-center; - - p { - @apply p-6; - } - - > a > img { - @apply w-60; - } -} +// to be removed diff --git a/app/javascript/dashboard/assets/scss/widgets/_base.scss b/app/javascript/dashboard/assets/scss/widgets/_base.scss new file mode 100644 index 000000000..261df19e8 --- /dev/null +++ b/app/javascript/dashboard/assets/scss/widgets/_base.scss @@ -0,0 +1,140 @@ +// scss-lint:disable QualifyingElement + +// Base typography +h1, +h2, +h3, +h4, +h5, +h6 { + @apply font-medium text-slate-800 dark:text-slate-50; +} + +p { + text-rendering: optimizeLegibility; + word-spacing: 0.12em; + + @apply mb-2 leading-[1.65] text-sm; + + a { + @apply text-woot-500 dark:text-woot-500 cursor-pointer; + } +} + +a { + @apply text-sm; +} + +hr { + @apply clear-both max-w-full h-0 my-5 mx-0 border-slate-300 dark:border-slate-600; +} + +// Form elements +label { + @apply text-slate-800 dark:text-slate-200 block m-0 leading-7 text-sm font-medium; + + &.error { + input { + @apply mb-1; + } + } +} + +.input-wrap, +.help-text { + @apply text-slate-800 dark:text-slate-100 text-sm font-medium; + + .help-text { + @apply font-normal text-slate-600 dark:text-slate-400; + } +} + +// Focus outline removal +.button, +textarea, +input:focus { + outline: none; +} + +// Inputs +input[type='text'], +input[type='number'], +input[type='password'], +input[type='date'], +input[type='email'], +input[type='url'] { + @apply block box-border w-full transition-colors focus:border-woot-500 dark:focus:border-woot-600 duration-[0.25s] ease-[ease-in-out] h-10 appearance-none mx-0 mt-0 mb-4 p-2 rounded-md text-base font-normal bg-white dark:bg-slate-900 focus:bg-white focus:dark:bg-slate-900 text-slate-900 dark:text-slate-100 border border-solid border-slate-200 dark:border-slate-600; + + &[disabled] { + @apply bg-slate-200 dark:bg-slate-700 text-slate-400 dark:text-slate-400 border-slate-200 dark:border-slate-600 cursor-not-allowed; + } +} + +input[type='file'] { + @apply bg-white dark:bg-slate-800 leading-[1.15] mb-4; +} + +// Select +select { + background-image: url("data:image/svg+xml;utf8,"); + background-position: right -1rem center; + background-size: 9px 6px; + @apply h-10 mx-0 mt-0 mb-4 bg-origin-content focus-visible:outline-none bg-no-repeat py-2 pr-6 pl-2 rounded-md w-full text-base font-normal appearance-none transition-colors focus:border-woot-500 dark:focus:border-woot-600 duration-[0.25s] ease-[ease-in-out] bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 border border-solid border-slate-200 dark:border-slate-600; +} + +// Textarea +textarea { + @apply block box-border w-full transition-colors focus:border-woot-500 dark:focus:border-woot-600 duration-[0.25s] ease-[ease-in-out] h-16 appearance-none mx-0 mt-0 mb-4 p-2 rounded-md text-base font-normal bg-white dark:bg-slate-900 focus:bg-white focus:dark:bg-slate-900 text-slate-900 dark:text-slate-100 border border-solid border-slate-200 dark:border-slate-600; + + &[disabled] { + @apply bg-slate-200 dark:bg-slate-700 text-slate-400 dark:text-slate-400 border-slate-200 dark:border-slate-600 cursor-not-allowed; + } +} + +// Error handling +.has-multi-select-error { + div.multiselect { + @apply mb-1; + } +} + +.error { + input, + input:not([type]), + textarea, + select, + .multiselect > .multiselect__tags, + .multiselect:not(.no-margin) { + @apply border border-solid border-red-400 dark:border-red-400 mb-1; + } + + .message { + @apply text-red-400 dark:text-red-400 block text-sm mb-2.5 w-full; + } +} + +.input-group.small { + input { + @apply text-sm h-8; + } + + .error { + @apply border-red-400 dark:border-red-400; + } +} + +// Code styling +code { + font-family: 'ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', + '"Liberation Mono"', '"Courier New"', 'monospace'; + @apply text-xs border-0; + + &.hljs { + @apply bg-slate-50 dark:bg-slate-700 text-slate-800 dark:text-slate-50 rounded-lg p-5; + + .hljs-number, + .hljs-string { + @apply text-red-800 dark:text-red-400; + } + } +} diff --git a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss index 1b6166a18..423a41843 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss @@ -1,8 +1,40 @@ +// scss-lint:disable SpaceAfterPropertyColon +// scss-lint:disable MergeableSelector +button { + font-family: inherit; + transition: + background-color 0.25s ease-out, + color 0.25s ease-out; + @apply inline-block items-center mb-0 text-center align-middle cursor-pointer text-sm mt-0 mx-0 py-1 px-2.5 border border-solid border-transparent dark:border-transparent rounded-[0.3125rem]; + + &:disabled, + &.disabled { + @apply opacity-40 cursor-not-allowed; + } +} + +.button-group { + @apply mb-0 flex flex-nowrap items-stretch; + + .button { + flex: 0 0 auto; + @apply m-0 text-sm rounded-none first:rounded-tl-[0.3125rem] first:rounded-bl-[0.3125rem] last:rounded-tr-[0.3125rem] last:rounded-br-[0.3125rem] rtl:space-x-reverse; + } + + .button--only-icon { + @apply w-10 justify-center pl-0 pr-0; + } +} + +.back-button { + @apply m-0; +} + .button { - @apply items-center inline-flex h-10 mb-0 gap-2; + @apply items-center bg-woot-500 dark:bg-woot-500 px-2.5 text-white dark:text-white inline-flex h-10 mb-0 gap-2 font-medium; .button__content { - @apply w-full; + @apply w-full whitespace-nowrap overflow-hidden text-ellipsis; img, svg { @@ -10,12 +42,61 @@ } } + &:hover { + @apply bg-woot-600 dark:bg-woot-600; + } + + &:disabled, + &.disabled { + @apply opacity-40 cursor-not-allowed; + } + + &.success { + @apply bg-[#44ce4b] text-white dark:text-white; + } + + &.secondary { + @apply bg-slate-700 dark:bg-slate-600 text-white dark:text-white; + } + + &.primary { + @apply bg-woot-500 dark:bg-woot-500 text-white dark:text-white; + } + + &.clear { + @apply text-woot-500 dark:text-woot-500 bg-transparent dark:bg-transparent; + } + + &.alert { + @apply bg-red-500 dark:bg-red-500 text-white dark:text-white; + + &.clear { + @apply bg-transparent dark:bg-transparent; + } + } + + &.warning { + @apply bg-[#ffc532] dark:bg-[#ffc532] text-white dark:text-white; + + &.clear { + @apply bg-transparent dark:bg-transparent; + } + } + + &.tiny { + @apply h-6 text-[10px]; + } + + &.small { + @apply h-8 text-xs; + } + .spinner { @apply px-2 py-0; } // @TODDO - Remove after moving all buttons to woot-button - .icon + .button__content { + .icon+.button__content { @apply w-auto; } @@ -34,7 +115,7 @@ } &.hollow { - @apply border border-woot-500 dark:border-woot-500 text-woot-500 dark:text-woot-500 hover:bg-woot-50 dark:hover:bg-woot-900; + @apply border border-woot-500 bg-transparent dark:bg-transparent dark:border-woot-500 text-woot-500 dark:text-woot-500 hover:bg-woot-50 dark:hover:bg-woot-900; &.secondary { @apply text-slate-700 border-slate-200 dark:border-slate-600 dark:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-700; diff --git a/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss b/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss deleted file mode 100644 index 04f610dd2..000000000 --- a/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss +++ /dev/null @@ -1 +0,0 @@ -// File to be removed diff --git a/app/javascript/dashboard/assets/scss/widgets/_conversation-card.scss b/app/javascript/dashboard/assets/scss/widgets/_conversation-card.scss deleted file mode 100644 index d060dc2ee..000000000 --- a/app/javascript/dashboard/assets/scss/widgets/_conversation-card.scss +++ /dev/null @@ -1,16 +0,0 @@ -@keyframes left-shift-animation { - 0%, - 100% { - transform: translateX(0); - } - - 50% { - transform: translateX(1px); - } -} - -.conversation { - &.active { - animation: left-shift-animation 0.25s $swift-ease-out-function; - } -} diff --git a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss index 5cc3caa27..ea3a9c5f1 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss @@ -79,7 +79,7 @@ @apply rounded-r-lg rounded-l mr-auto break-words; &:not(.is-unsupported) { - @apply border border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-700 text-black-900 dark:text-slate-50 + @apply border border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-700 text-black-900 dark:text-slate-50; } &.is-image { @@ -91,7 +91,7 @@ } .file { - .text-block-title { + .attachment-name { @apply text-slate-700 dark:text-woot-300; } diff --git a/app/javascript/dashboard/assets/scss/widgets/_forms.scss b/app/javascript/dashboard/assets/scss/widgets/_forms.scss deleted file mode 100644 index 4688b648c..000000000 --- a/app/javascript/dashboard/assets/scss/widgets/_forms.scss +++ /dev/null @@ -1,78 +0,0 @@ -// scss-lint:disable QualifyingElement - -label { - @apply text-slate-800 dark:text-slate-200; -} - -textarea { - @apply bg-white dark:bg-slate-900 focus:bg-white focus:dark:bg-slate-900 text-slate-900 dark:text-slate-100 border-slate-200 dark:border-slate-600; -} - -input { - @apply bg-white dark:bg-slate-900 focus:bg-white focus:dark:bg-slate-900 text-slate-900 dark:text-slate-100 border-slate-200 dark:border-slate-600; - - &[disabled] { - @apply bg-slate-200 dark:bg-slate-700 text-slate-400 dark:text-slate-400 border-slate-200 dark:border-slate-600; - } -} - -input[type='file'] { - @apply bg-white dark:bg-slate-800; -} - -select { - @apply bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 border-slate-200 dark:border-slate-600; -} - -.error { - input[type='color'], - input[type='date'], - input[type='datetime'], - input[type='datetime-local'], - input[type='email'], - input[type='month'], - input[type='number'], - input[type='password'], - input[type='search'], - input[type='tel'], - input[type='text'], - input[type='time'], - input[type='url'], - input[type='week'], - input:not([type]), - textarea, - select, - .multiselect > .multiselect__tags { - @apply border border-solid border-red-400 dark:border-red-400; - } - - .message { - @apply text-red-400 dark:text-red-400 block text-sm mb-2.5 w-full; - } -} - -.button, -textarea, -input { - &:focus { - outline: none; - } -} - -.input-wrap { - @apply text-slate-800 dark:text-slate-100 text-sm font-medium; -} - -.help-text { - @apply font-normal text-slate-600 dark:text-slate-400; -} - -.input-group.small { - input { - @apply text-sm h-8; - } - - .error { - @apply border-red-400 dark:border-red-400; - } -} diff --git a/app/javascript/dashboard/assets/scss/widgets/_login.scss b/app/javascript/dashboard/assets/scss/widgets/_login.scss deleted file mode 100644 index a5b140ede..000000000 --- a/app/javascript/dashboard/assets/scss/widgets/_login.scss +++ /dev/null @@ -1,66 +0,0 @@ -.auth-wrap { - width: 100%; -} - -// Outside login wrapper -.login { - @include full-height; - overflow-y: auto; - padding-top: $space-larger * 1.2; - - .login__hero { - margin-bottom: $space-larger; - - .hero__logo { - width: 180px; - } - - .hero__title { - font-weight: $font-weight-light; - margin-top: $space-larger; - } - - .hero__sub { - color: $medium-gray; - font-size: $font-size-medium; - } - } - - // Login box - .login-box { - @include background-white; - @include border-normal; - @include elegant-card; - - border-radius: $space-smaller; - padding: $space-large; - - label { - color: $color-gray; - font-size: $font-size-default; - - input { - font-size: $font-size-default; - height: $space-larger; - padding: $space-slab; - } - - .error { - font-size: $font-size-small; - } - } - - .button { - height: $space-larger; - } - } - - .sigin__footer { - font-size: $font-size-default; - padding: $space-medium; - - > a { - font-weight: $font-weight-bold; - } - } -} diff --git a/app/javascript/dashboard/assets/scss/widgets/_modal.scss b/app/javascript/dashboard/assets/scss/widgets/_modal.scss deleted file mode 100644 index 4a3ca61c1..000000000 --- a/app/javascript/dashboard/assets/scss/widgets/_modal.scss +++ /dev/null @@ -1,75 +0,0 @@ -.modal-mask { - // @include flex; - // @include flex-align(center, middle); - @apply flex items-center justify-center bg-modal-backdrop-light dark:bg-modal-backdrop-dark z-[9990] h-full left-0 fixed top-0 w-full; -} - -.page-top-bar { - @apply px-8 pt-9 pb-0; - - img { - @apply max-h-[3.75rem]; - } -} - -.modal-container { - @apply shadow-md rounded-sm max-h-full overflow-auto relative w-[37.5rem]; - - &.medium { - @apply max-w-[80%] w-[56.25rem]; - } - - .content-box { - @apply h-auto p-0; - } - - h2 { - @apply text-slate-800 dark:text-slate-100 text-lg font-semibold; - } - - p { - @apply text-sm m-0 p-0 text-slate-600 mt-2 text-sm dark:text-slate-300; - } - - .content { - @apply p-8; - } - - form, - .modal-content { - @apply pt-4 pb-8 px-8 self-center; - - a { - @apply p-4; - } - } - - .modal-footer { - // @include flex; - // @include flex-align($x: flex-end, $y: middle); - @apply flex justify-end items-center py-2 px-0 gap-2; - - &.justify-content-end { - @apply justify-end; - } - } - - .delete-item { - @apply p-8; - - button { - @apply m-0; - } - } -} - -.modal-enter, -.modal-leave { - @apply opacity-0; -} - -.modal-enter .modal-container, -.modal-leave .modal-container { - transform: scale(1.1); - // @apply transform scale-110; -} diff --git a/app/javascript/dashboard/assets/scss/widgets/_reply-box.scss b/app/javascript/dashboard/assets/scss/widgets/_reply-box.scss deleted file mode 100644 index d15150f36..000000000 --- a/app/javascript/dashboard/assets/scss/widgets/_reply-box.scss +++ /dev/null @@ -1,60 +0,0 @@ -.reply-box { - transition: box-shadow 0.35s $swift-ease-out-function, - height 2s $swift-ease-out-function; - - &.is-focused { - box-shadow: var(--shadow); - } - - .reply-box__top { - .icon { - color: $medium-gray; - cursor: pointer; - font-size: $font-size-medium; - margin-right: $space-small; - - &.active { - color: $color-woot; - } - } - - .attachment { - cursor: pointer; - margin-right: $space-one; - padding: 0 $space-small; - } - - .video-js { - background: transparent; - // Override min-height : 50px in foundation - // - max-height: $space-mega * 2.4; - min-height: 3rem; - padding: var(--space-normal) 0 0; - resize: none; - } - - > textarea { - @include ghost-input(); - background: transparent; - margin: 0; - max-height: $space-mega * 2.4; - // Override min-height : 50px in foundation - min-height: 3rem; - padding: var(--space-normal) 0 0; - resize: none; - } - } - - &.is-private { - @apply bg-yellow-100 dark:bg-yellow-800; - - .reply-box__top { - @apply bg-yellow-100 dark:bg-yellow-800; - - > input { - @apply bg-yellow-100 dark:bg-yellow-800; - } - } - } -} diff --git a/app/javascript/dashboard/assets/scss/widgets/_report.scss b/app/javascript/dashboard/assets/scss/widgets/_report.scss deleted file mode 100644 index d07f1dd3d..000000000 --- a/app/javascript/dashboard/assets/scss/widgets/_report.scss +++ /dev/null @@ -1,60 +0,0 @@ -.report-card { - @include custom-border-top(3px, transparent); - - cursor: pointer; - margin: 0; - padding: var(--space-normal); - - &.active { - @include custom-border-top(3px, var(--color-woot)); - @include background-white; - .heading, - .metric { - color: var(--color-woot); - } - } - - .heading { - align-items: center; - color: var(--color-heading); - display: flex; - font-size: var(--font-size-small); - font-weight: var(--font-weight-bold); - margin: 0; - } - - .info-icon { - color: var(--b-400); - margin-left: var(--space-micro); - } - - .metric-wrap { - align-items: center; - display: flex; - } - - .metric { - font-size: var(--font-size-big); - font-weight: var(--font-weight-feather); - margin-top: var(--space-smaller); - } - - .metric-trend { - font-size: var(--font-size-small); - margin: 0 var(--space-small); - } - - .metric-up { - color: $success-color; - } - - .metric-down { - color: $alert-color; - } - - .desc { - font-size: var(--font-size-small); - margin: 0; - text-transform: capitalize; - } -} diff --git a/app/javascript/dashboard/assets/scss/widgets/_reports.scss b/app/javascript/dashboard/assets/scss/widgets/_reports.scss deleted file mode 100644 index dcd65366a..000000000 --- a/app/javascript/dashboard/assets/scss/widgets/_reports.scss +++ /dev/null @@ -1,29 +0,0 @@ -.reports-option__rounded--item { - border-radius: 100%; - height: var(--space-two); - width: var(--space-two); -} - -.reports-option__item { - flex-shrink: 0; - margin-right: var(--space-small); -} - -.reports-option__label--swatch { - border: 1px solid var(--color-border); -} - -.reports-option__wrap { - align-items: center; - display: flex; -} - -.reports-option__title { - margin: 0 var(--space-small); -} - - -.switch { - margin-bottom: var(--space-zero); - margin-left: var(--space-small); -} diff --git a/app/javascript/dashboard/assets/scss/widgets/_search-box.scss b/app/javascript/dashboard/assets/scss/widgets/_search-box.scss deleted file mode 100644 index 643757e60..000000000 --- a/app/javascript/dashboard/assets/scss/widgets/_search-box.scss +++ /dev/null @@ -1,18 +0,0 @@ -.search { - @include flex; - @include flex-align($x: left, $y: middle); - @include flex-shrink; - - padding: $space-one $space-normal; - transition: all 0.3s var(--ease-in-out-quad); - - > .icon { - color: $medium-gray; - font-size: $font-size-medium; - } - - > input { - @include ghost-input(); - margin: 0; - } -} diff --git a/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss b/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss deleted file mode 100644 index 3de072a77..000000000 --- a/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss +++ /dev/null @@ -1,78 +0,0 @@ -.side-menu { - i { - margin-right: var(--space-smaller); - min-width: var(--space-two); - } -} - -.sidebar { - z-index: 1024 - 1; - - //logo - .logo { - img { - max-height: 108px; - padding: $woot-logo-padding; - } - } - - .nested { - a { - font-size: var(--font-size-small); - margin-bottom: var(--space-micro); - margin-top: var(--space-micro); - - .inbox-icon { - display: inline-block; - margin-right: var(--space-micro); - min-width: var(--space-normal); - text-align: center; - } - } - } -} - -// bottom-nav -.bottom-nav { - @include flex; - @include space-between-column; - @include border-normal-top; - flex-direction: column; - padding: var(--space-one) var(--space-normal) var(--space-one) - var(--space-one); - position: relative; - - &:hover { - background: var(--color-background-light); - } - - .dropdown-pane { - bottom: 3.75rem; - display: block; - visibility: visible; - width: fit-content; - } - - .active { - border-bottom: 2px solid $medium-gray; - } -} - -.hamburger--menu { - cursor: pointer; - display: block; - margin-right: var(--space-normal); -} - -.header--icon { - display: block; - margin: 0 var(--space-small) 0 var(--space-smaller); - - @media screen and (max-width: 1200px) { - display: none; - } -} - -.header-title { - margin: 0 var(--space-small); -} diff --git a/app/javascript/dashboard/assets/scss/widgets/_snackbar.scss b/app/javascript/dashboard/assets/scss/widgets/_snackbar.scss deleted file mode 100644 index ee556682f..000000000 --- a/app/javascript/dashboard/assets/scss/widgets/_snackbar.scss +++ /dev/null @@ -1,45 +0,0 @@ -.ui-snackbar-container { - left: 0; - margin: 0 auto; - max-width: 25rem; - overflow: hidden; - position: absolute; - right: 0; - text-align: center; - top: $space-normal; - z-index: 9999; -} - -.ui-snackbar { - @include shadow; - background-color: $woot-snackbar-bg; - border-radius: $space-smaller; - display: inline-flex; - margin-bottom: $space-small; - max-width: 25rem; - min-height: 1.875rem; - min-width: 15rem; - padding: $space-slab $space-medium; - text-align: left; -} - -.ui-snackbar-text { - color: $color-white; - font-size: $font-size-small; - font-weight: $font-weight-medium; -} - -.ui-snackbar-action { - margin-left: auto; - padding-left: 1.875rem; - - button { - background: none; - border: 0; - color: $woot-snackbar-button; - font-size: $font-size-small; - margin: 0; - padding: 0; - text-transform: uppercase; - } -} diff --git a/app/javascript/dashboard/assets/scss/widgets/_states.scss b/app/javascript/dashboard/assets/scss/widgets/_states.scss deleted file mode 100644 index 7eb840221..000000000 --- a/app/javascript/dashboard/assets/scss/widgets/_states.scss +++ /dev/null @@ -1 +0,0 @@ -// To be removed diff --git a/app/javascript/dashboard/assets/scss/widgets/_status-bar.scss b/app/javascript/dashboard/assets/scss/widgets/_status-bar.scss deleted file mode 100644 index ad0f805e2..000000000 --- a/app/javascript/dashboard/assets/scss/widgets/_status-bar.scss +++ /dev/null @@ -1,46 +0,0 @@ -.status-bar { - @include flex; - @include flex-align($x: center, $y: middle); - background: lighten($warning-color, 36%); - flex-direction: column; - margin: 0; - padding: $space-normal $space-smaller; - - .message { - font-weight: $font-weight-medium; - margin-bottom: $zero; - } - - .button { - margin: $space-smaller $zero $zero; - padding: $space-small $space-normal; - } - - &.danger { - background: lighten($alert-color, 30%); - - .button { - // Default and disabled states - &, - &.disabled, - &[disabled], - &.disabled:hover, - &[disabled]:hover, - &.disabled:focus, - &[disabled]:focus { - background-color: $alert-color; - color: $color-white; - } - - &:hover, - &:focus { - background-color: darken($alert-color, 7%); - color: $color-white; - } - } - } - - &.warning { - background: lighten($warning-color, 36%); - } -} diff --git a/app/javascript/dashboard/assets/scss/widgets/_tabs.scss b/app/javascript/dashboard/assets/scss/widgets/_tabs.scss index 35a9ee9ed..3ef40a137 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_tabs.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_tabs.scss @@ -31,7 +31,7 @@ } .tabs-title { - @apply flex-shrink-0 my-0 mx-2 ; + @apply flex-shrink-0 my-0 mx-2; .badge { @apply bg-slate-50 dark:bg-slate-800 rounded-md text-slate-600 dark:text-slate-100 h-5 flex items-center justify-center text-xxs font-semibold my-0 mx-1 px-1 py-0; @@ -53,7 +53,7 @@ } a { - @apply flex items-center flex-row border-b border-transparent text-slate-500 dark:text-slate-200 text-sm top-[1px] relative; + @apply flex items-center flex-row border-b py-2.5 select-none cursor-pointer border-transparent text-slate-500 dark:text-slate-200 text-sm top-[1px] relative; transition: border-color 0.15s $swift-ease-out-function; } diff --git a/app/javascript/dashboard/assets/scss/widgets/_widget_builder.scss b/app/javascript/dashboard/assets/scss/widgets/_widget_builder.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/javascript/dashboard/assets/scss/widgets/_woot-tables.scss b/app/javascript/dashboard/assets/scss/widgets/_woot-tables.scss index e7a1adbca..97535bf0d 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_woot-tables.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_woot-tables.scss @@ -1,9 +1,9 @@ table { - @apply border-spacing-0 text-sm; + @apply border-spacing-0 text-sm w-full; thead { th { - @apply font-semibold tracking-[1px] text-left uppercase text-slate-900 dark:text-slate-200; + @apply font-semibold tracking-[1px] text-left px-2.5 uppercase text-slate-900 dark:text-slate-200; } } diff --git a/app/javascript/dashboard/components/Accordion/AccordionItem.vue b/app/javascript/dashboard/components/Accordion/AccordionItem.vue index a8f7a0e3a..87370185d 100644 --- a/app/javascript/dashboard/components/Accordion/AccordionItem.vue +++ b/app/javascript/dashboard/components/Accordion/AccordionItem.vue @@ -1,7 +1,7 @@