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 9e59758ea5df..0aea9df8345a 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -55,7 +55,7 @@ def app_config 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 a31d01675bb5..6223f717495c 100644 --- a/app/controllers/super_admin/app_configs_controller.rb +++ b/app/controllers/super_admin/app_configs_controller.rb @@ -34,7 +34,7 @@ def set_config 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 60eae3bc25ef..9db527832873 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 221c96b8564f..eff9975f7a8a 100644 --- a/app/controllers/api/v1/accounts/agents_controller.rb +++ b/app/controllers/api/v1/accounts/agents_controller.rb @@ -49,6 +49,11 @@ def bulk_create 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 481594ee4b14..c0dac6d9e073 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -46,6 +46,7 @@ def cache_keys 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 ea19727ad0a4..45faca3d0d28 100644 --- a/app/controllers/api/v2/accounts_controller.rb +++ b/app/controllers/api/v2/accounts_controller.rb @@ -22,6 +22,7 @@ def create ).perform fetch_account_and_user_info + update_account_info if @account.present? if @user send_auth_headers(@user) @@ -33,6 +34,18 @@ def create 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 9f89a5510b8a..5e9d9048a403 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 d0d76c1ae9d7..3edefbb5f809 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 @@ def fetch_from_clearbit 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 95fa2ae7fad1..bd604f6506fa 100644 --- a/spec/controllers/api/v1/accounts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts_controller_spec.rb @@ -213,6 +213,25 @@ 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 82693ed2b47a..182ebadac1dc 100644 --- a/spec/controllers/api/v2/accounts_controller_spec.rb +++ b/spec/controllers/api/v2/accounts_controller_spec.rb @@ -30,6 +30,20 @@ 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 97e76a6cd67e..e5a8a5b7d6eb 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 @@ 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 8ad75b42fbe7..c74a2bcb50d5 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 @@ 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 f424f3b66edd..98683ea25729 100644 --- a/app/controllers/api/v1/accounts/contacts_controller.rb +++ b/app/controllers/api/v1/accounts/contacts_controller.rb @@ -46,7 +46,7 @@ def import 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 594e34c4f955..cae908016b8a 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 4df4017b2736..1b4fd90e0fd9 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 0ae8b38929e6..b8e9dbc96c9c 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 8c52a9bd35c9..6c0f7cee297d 100644 --- a/app/mailers/administrator_notifications/channel_notifications_mailer.rb +++ b/app/mailers/administrator_notifications/channel_notifications_mailer.rb @@ -60,12 +60,13 @@ def contact_import_failed 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 e5e298747535..21511310b3eb 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 @@ 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 @@ 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 e597ff63783f..b395967b4173 100644 --- a/spec/jobs/account/contacts_export_job_spec.rb +++ b/spec/jobs/account/contacts_export_job_spec.rb @@ -24,17 +24,17 @@ 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 f67b8300cd6f..944475fb2fef 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 @@ 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 @@ 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 f8c6e3d5b285..cd3d57627bbd 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -68,20 +68,12 @@ def push_event_data } 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 3dcb47f71c65..4605e0fdaa98 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 @@ 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 3ac0568e305b..52035ce64cf8 100644 --- a/app/controllers/api/v1/accounts/notifications_controller.rb +++ b/app/controllers/api/v1/accounts/notifications_controller.rb @@ -54,7 +54,8 @@ def unread_count 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 05235b62b44d..91ed5ee61341 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 cd3d57627bbd..113c6cf3e810 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -64,6 +64,7 @@ def push_event_data 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 ae86372c9b63..4a408dfca945 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 4605e0fdaa98..9b16e48fbafc 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 @@ 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 024c3d0d62d0..94697dcd63f2 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 @@ 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 212c44d94674..a2a142430370 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 95650748acbd..d02e543baf7e 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 7a9a14d0da13..79f1234a00b9 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 000000000000..1d64fc67d1b5 --- /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 138ea683505d..546df444f6c5 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 @@ 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 825c747fc739..cb5ca508db03 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 6e21031aa9f3..1e4165b09315 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 6efffca6de87..a77a039ddf71 100644 --- a/spec/enterprise/services/enterprise/action_service_spec.rb +++ b/spec/enterprise/services/enterprise/action_service_spec.rb @@ -8,16 +8,42 @@ 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 1ab16149ddad..cc9a6f34cad9 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 49fe325f6fe7..481c671ce6de 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 db508ac13a6f..16bc9cd412a2 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 8481a8ff4b07..4ebeccdcb72a 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 c1e273e709b3..6a1902ce42ce 100644 --- a/app/mailboxes/application_mailbox.rb +++ b/app/mailboxes/application_mailbox.rb @@ -37,10 +37,24 @@ def in_reply_to_matches?(in_reply_to) # 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 000000000000..8dd2aabb624d --- /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 000000000000..4ef77cb2d6c1 --- /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 d5e15dc46c95..733ec5523a15 100644 --- a/spec/mailboxes/application_mailbox_spec.rb +++ b/spec/mailboxes/application_mailbox_spec.rb @@ -10,6 +10,8 @@ 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 @@ 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 000000000000..8480b1846a6f --- /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 75f09e9daba1..444285657405 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 9d06f15c6a9c..2936d22ead00 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 59361e446125..4b378c74aaa6 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 6cc73c5b6383..75cdb5836167 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 6c4d8cbfe253..d1d16a607a7a 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 000000000000..67ec4549f06c --- /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 48ef316ef937..98b81f47c4dc 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 000000000000..772ec4bcd342 --- /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 000000000000..2fc577bada36 --- /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 000000000000..473583f6dde1 --- /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 000000000000..775c0ff2a315 --- /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 000000000000..360c5a1f583f --- /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 000000000000..a828c855602c --- /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 000000000000..28f7436c8997 --- /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 000000000000..bda4b95560e1 --- /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 2d1564b798be..91b84ddd1e21 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 000000000000..e1ded7f51595 --- /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 0d4f0c010ef9..c9d2d1767807 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 529ad37d95a0..f0477c2970c0 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 9628e1da4ad3..18e5b15baf99 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 671863bdcdd5..4e3b5dca9770 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 @@ def update_last_seen 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 000000000000..1b9588465962 --- /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 000000000000..1b9588465962 --- /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 8c4d4124be16..1ee74145f131 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -366,8 +366,9 @@ 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 d54f26272aa8..2b2bdefc35c1 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 @@ 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 d58439647788..2c6dd3f87e34 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 000000000000..2ec6caa63149 --- /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 000000000000..cac1c3d8da30 --- /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 000000000000..af01c77f6336 --- /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 000000000000..4e56aa11db24 --- /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 4859a785f0b3..dde7c5f01d1f 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 5add6e26a50e..f4187dbb5ca7 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 6b2f374333ed..a88d3535b23f 100644 --- a/app/actions/contact_identify_action.rb +++ b/app/actions/contact_identify_action.rb @@ -59,7 +59,7 @@ def existing_identified_contact 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 6ea68821d064..07b0e7345383 100644 --- a/app/builders/agent_builder.rb +++ b/app/builders/agent_builder.rb @@ -27,7 +27,7 @@ def perform # 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 d97f64cfe629..6e913eda83b6 100644 --- a/app/builders/contact_inbox_with_contact_builder.rb +++ b/app/builders/contact_inbox_with_contact_builder.rb @@ -75,7 +75,7 @@ def find_contact_by_identifier(identifier) 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 26a9d45555f5..17dd320864fb 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 2659aeebf2b7..e623e52f7583 100644 --- a/app/controllers/devise_overrides/sessions_controller.rb +++ b/app/controllers/devise_overrides/sessions_controller.rb @@ -33,7 +33,7 @@ def authenticate_resource_with_sso_token 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 03ed5575405e..453e475b0f9e 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 57d30253adca..a8308b48ad8b 100644 --- a/app/mailboxes/imap/imap_mailbox.rb +++ b/app/mailboxes/imap/imap_mailbox.rb @@ -91,7 +91,7 @@ def find_or_create_conversation 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 5a1f5ecf5d9d..94404d8ed1d4 100644 --- a/app/mailboxes/support_mailbox.rb +++ b/app/mailboxes/support_mailbox.rb @@ -86,7 +86,7 @@ def find_or_create_conversation 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 af3f605a7605..23eca55e6646 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -165,6 +165,12 @@ def discard_invalid_attrs 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 ba638f637844..b54f74a64bcb 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -156,6 +156,12 @@ def will_save_change_to_email? 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 ab9a1ee70461..460a837265c9 100644 --- a/app/services/data_import/contact_manager.rb +++ b/app/services/data_import/contact_manager.rb @@ -35,7 +35,7 @@ def find_contact_by_identifier(params) 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 9522112a8b17..762d488c5cf2 100644 --- a/lib/integrations/slack/slack_message_helper.rb +++ b/lib/integrations/slack/slack_message_helper.rb @@ -69,7 +69,7 @@ def conversation 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 000000000000..0bc248c27fe6 --- /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 9793ffd1dc75..802a02dbf76c 100644 --- a/lib/seeders/account_seeder.rb +++ b/lib/seeders/account_seeder.rb @@ -88,7 +88,7 @@ def seed_contacts 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 @@ def find_message_sender(conversation, message_data) 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 2ebb0d1a48a2..95d6cd452dc0 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 @@ ) ) 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 5ece07e53d8d..fb1057f4a1fc 100644 --- a/spec/jobs/data_import_job_spec.rb +++ b/spec/jobs/data_import_job_spec.rb @@ -111,7 +111,7 @@ 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 98683ea25729..71e9100e77ab 100644 --- a/app/controllers/api/v1/accounts/contacts_controller.rb +++ b/app/controllers/api/v1/accounts/contacts_controller.rb @@ -148,7 +148,7 @@ def build_contact_inbox 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 dafe97cba2ce..df3c85f1fe82 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 cae908016b8a..791279899da8 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 5253468691c6..c6ea4c7b1535 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 23eca55e6646..07b29fdf30e2 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 fb2ae8b7708a..72518f2504f2 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 @@ def validate_additional_attributes 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 000000000000..6d37484e4b7e --- /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 546df444f6c5..775e67f960be 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 @@ 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 21511310b3eb..d2527e6a99a4 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 @@ 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 efeda965d313..2f79b08f2c81 100644 --- a/spec/models/conversation_spec.rb +++ b/spec/models/conversation_spec.rb @@ -372,9 +372,9 @@ 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 @@ 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 @@ 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 d1a85570b78d..665e8faf66b6 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 de108910abf2..a7e8d8120459 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 67184054a9c2..152b55aae854 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 d1b9866b0fe8..2c90f987e19c 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 8c52d2e1a3aa..a2e19433398c 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 21e942e0ff94..59bfc5d7ff61 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 738a3415fd42..41cb0f20bc00 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 2c90f987e19c..c9f55bb6833f 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 9788a1ef7293..6f2a05bf5666 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 67ec4549f06c..d87f057eb437 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 6c4002821d46..9ff510d06daa 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 775c0ff2a315..f41e8fcfc7b4 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 a828c855602c..2266bdccdf3d 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 28f7436c8997..d086c6072b16 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 bda4b95560e1..024aa6636d3e 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 f7a2f86f7996..70ab8a666c38 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 cc9a6f34cad9..b28813887c73 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 | 53 +- .../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, 1133 insertions(+), 2510 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 f7d10e633917..000000000000 --- 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 3fd834cddb11..000000000000 --- 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 0b400c4e9eb6..48ee1918befa 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 594a14d95c4e..ea40c1f3a371 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 6f16a2af46e3..36679fc60fe4 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 ee35a9cf0cc9..000000000000 --- 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 99f0fe3eb395..000000000000 --- 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 2773ced6fb44..cdc522224f12 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 1006623946b1..e08099ae61fe 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 0fddae3d1ff3..9170715e01d3 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 457560aa9ffa..fa2455918de3 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 39e4ed2bd1c4..8a118b906245 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 000000000000..261df19e81b1 --- /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 1b6166a18ae7..423a41843bfb 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 04f610dd299a..000000000000 --- 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 d060dc2ee83a..000000000000 --- 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 5cc3caa27487..ea3a9c5f1821 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 4688b648cdaa..000000000000 --- 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 a5b140edefb8..000000000000 --- 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 4a3ca61c14d6..000000000000 --- 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 d15150f36194..000000000000 --- 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 d07f1dd3d4a6..000000000000 --- 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 dcd65366a944..000000000000 --- 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 643757e60629..000000000000 --- 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 3de072a77cc1..000000000000 --- 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 ee556682f54a..000000000000 --- 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 7eb840221fd3..000000000000 --- 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 ad0f805e22d9..000000000000 --- 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 35a9ee9eda99..3ef40a137685 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 e69de29bb2d1..000000000000 diff --git a/app/javascript/dashboard/assets/scss/widgets/_woot-tables.scss b/app/javascript/dashboard/assets/scss/widgets/_woot-tables.scss index e7a1adbca184..97535bf0d3c5 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 a8f7a0e3afa5..87370185d2a9 100644 --- a/app/javascript/dashboard/components/Accordion/AccordionItem.vue +++ b/app/javascript/dashboard/components/Accordion/AccordionItem.vue @@ -1,7 +1,7 @@