diff --git a/.circleci/config.yml b/.circleci/config.yml
index 99795db91..65ceda04c 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,6 +1,7 @@
version: 2.1
orbs:
node: circleci/node@6.1.0
+ qlty-orb: qltysh/qlty-orb@0.0
defaults: &defaults
working_directory: ~/build
@@ -89,14 +90,6 @@ jobs:
command: |
source ~/.rvm/scripts/rvm
bundle install
- # pnpm install
-
- - run:
- name: Download cc-test-reporter
- command: |
- mkdir -p ~/tmp
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
- chmod +x ~/tmp/cc-test-reporter
# Swagger verification
- run:
@@ -108,10 +101,11 @@ jobs:
echo "ERROR: The swagger.json file is not in sync with the yaml specification. Run 'rake swagger:build' and commit 'swagger/swagger.json'."
exit 1
fi
+ mkdir -p ~/tmp
curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/6.3.0/openapi-generator-cli-6.3.0.jar > ~/tmp/openapi-generator-cli-6.3.0.jar
java -jar ~/tmp/openapi-generator-cli-6.3.0.jar validate -i swagger/swagger.json
- # we remove the FRONTED_URL from the .env before running the tests
+ # Configure environment and database
- run:
name: Database Setup and Configure Environment Variables
command: |
@@ -149,17 +143,11 @@ jobs:
command: pnpm run eslint
- run:
- name: Run frontend tests
+ name: Run frontend tests (with coverage)
command: |
mkdir -p ~/build/coverage/frontend
- ~/tmp/cc-test-reporter before-build
pnpm run test:coverage
- - run:
- name: Code Climate Test Coverage (Frontend)
- command: |
- ~/tmp/cc-test-reporter format-coverage -t lcov -o "~/build/coverage/frontend/codeclimate.frontend_$CIRCLE_NODE_INDEX.json"
-
# Run backend tests
- run:
name: Run backend tests
@@ -167,18 +155,18 @@ jobs:
mkdir -p ~/tmp/test-results/rspec
mkdir -p ~/tmp/test-artifacts
mkdir -p ~/build/coverage/backend
- ~/tmp/cc-test-reporter before-build
TESTFILES=$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
- bundle exec rspec --format progress \
+ bundle exec rspec -I ./spec --require coverage_helper --require spec_helper --format progress \
--format RspecJunitFormatter \
--out ~/tmp/test-results/rspec.xml \
-- ${TESTFILES}
no_output_timeout: 30m
- - run:
- name: Code Climate Test Coverage (Backend)
- command: |
- ~/tmp/cc-test-reporter format-coverage -t simplecov -o "~/build/coverage/backend/codeclimate.$CIRCLE_NODE_INDEX.json"
+ # Qlty coverage publish
+ - qlty-orb/coverage_publish:
+ files: |
+ coverage/coverage.json
+ coverage/lcov.info
- run:
name: List coverage directory contents
@@ -189,3 +177,7 @@ jobs:
root: ~/build
paths:
- coverage
+
+ - store_artifacts:
+ path: coverage
+ destination: coverage
diff --git a/Gemfile b/Gemfile
index a3b3c0ae8..03d0b62b9 100644
--- a/Gemfile
+++ b/Gemfile
@@ -62,6 +62,10 @@ gem 'redis-namespace'
# super fast record imports in bulk
gem 'activerecord-import'
+gem 'searchkick'
+gem 'opensearch-ruby'
+gem 'faraday_middleware-aws-sigv4'
+
##--- gems for server & infra configuration ---##
gem 'dotenv-rails', '>= 3.0.0'
gem 'foreman'
@@ -89,7 +93,7 @@ gem 'wisper', '2.0.0'
##--- gems for channels ---##
gem 'facebook-messenger'
gem 'line-bot-api'
-gem 'twilio-ruby', '~> 5.66'
+gem 'twilio-ruby'
# twitty will handle subscription of twitter account events
# gem 'twitty', git: 'https://github.com/chatwoot/twitty'
gem 'twitty', '~> 0.1.5'
@@ -212,6 +216,8 @@ group :development do
gem 'stackprof'
# Should install the associated chrome extension to view query logs
gem 'meta_request', '>= 0.8.3'
+
+ gem 'tidewave'
end
group :test do
@@ -221,6 +227,7 @@ group :test do
gem 'webmock'
# test profiling
gem 'test-prof'
+ gem 'simplecov_json_formatter', require: false
end
group :development, :test do
@@ -245,7 +252,7 @@ group :development, :test do
gem 'rubocop-factory_bot', require: false
gem 'seed_dump'
gem 'shoulda-matchers'
- gem 'simplecov', '0.17.1', require: false
+ gem 'simplecov', '>= 0.21', require: false
gem 'spring'
gem 'spring-watcher-listen'
end
diff --git a/Gemfile.lock b/Gemfile.lock
index be258f524..e8bccf54e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -219,7 +219,7 @@ GEM
diff-lcs (1.5.1)
digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0)
- docile (1.4.0)
+ docile (1.4.1)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (3.1.2)
@@ -230,6 +230,35 @@ GEM
addressable (~> 2.8)
drb (2.2.3)
dry-cli (1.1.0)
+ dry-configurable (1.3.0)
+ dry-core (~> 1.1)
+ zeitwerk (~> 2.6)
+ dry-core (1.1.0)
+ concurrent-ruby (~> 1.0)
+ logger
+ zeitwerk (~> 2.6)
+ dry-inflector (1.2.0)
+ dry-initializer (3.2.0)
+ dry-logic (1.6.0)
+ bigdecimal
+ concurrent-ruby (~> 1.0)
+ dry-core (~> 1.1)
+ zeitwerk (~> 2.6)
+ dry-schema (1.14.1)
+ concurrent-ruby (~> 1.0)
+ dry-configurable (~> 1.0, >= 1.0.1)
+ dry-core (~> 1.1)
+ dry-initializer (~> 3.2)
+ dry-logic (~> 1.5)
+ dry-types (~> 1.8)
+ zeitwerk (~> 2.6)
+ dry-types (1.8.3)
+ bigdecimal (~> 3.0)
+ concurrent-ruby (~> 1.0)
+ dry-core (~> 1.0)
+ dry-inflector (~> 1.0)
+ dry-logic (~> 1.4)
+ zeitwerk (~> 2.6)
ecma-re-validator (0.4.0)
regexp_parser (~> 2.2)
elastic-apm (4.6.2)
@@ -252,8 +281,10 @@ GEM
railties (>= 5.0.0)
faker (3.2.0)
i18n (>= 1.8.11, < 2)
- faraday (2.9.0)
- faraday-net_http (>= 2.0, < 3.2)
+ faraday (2.13.1)
+ faraday-net_http (>= 2.0, < 3.5)
+ json
+ logger
faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3)
faraday-mashify (0.1.1)
@@ -261,13 +292,23 @@ GEM
hashie
faraday-multipart (1.0.4)
multipart-post (~> 2)
- faraday-net_http (3.1.0)
- net-http
+ faraday-net_http (3.4.0)
+ net-http (>= 0.5.0)
faraday-net_http_persistent (2.1.0)
faraday (~> 2.5)
net-http-persistent (~> 4.0)
faraday-retry (2.2.1)
faraday (~> 2.0)
+ faraday_middleware-aws-sigv4 (1.0.1)
+ aws-sigv4 (~> 1.0)
+ faraday (>= 2.0, < 3)
+ fast-mcp (1.5.0)
+ addressable (~> 2.8)
+ base64
+ dry-schema (~> 1.14)
+ json (~> 2.0)
+ mime-types (~> 3.4)
+ rack (~> 3.1)
fcm (1.0.8)
faraday (>= 1.0.0, < 3.0)
googleauth (~> 1)
@@ -406,7 +447,7 @@ GEM
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
- json (2.12.0)
+ json (2.13.2)
json_refs (0.1.8)
hana
json_schemer (0.2.24)
@@ -421,7 +462,7 @@ GEM
judoscale-sidekiq (1.8.2)
judoscale-ruby (= 1.8.2)
sidekiq (>= 5.0)
- jwt (2.8.1)
+ jwt (2.10.1)
base64
kaminari (1.2.2)
activesupport (>= 4.1.0)
@@ -503,7 +544,7 @@ GEM
mutex_m (0.3.0)
neighbor (0.2.3)
activerecord (>= 5.2)
- net-http (0.4.1)
+ net-http (0.6.0)
uri
net-http-persistent (4.0.2)
connection_pool (~> 2.2)
@@ -563,6 +604,9 @@ GEM
omniauth-rails_csrf_protection (1.0.2)
actionpack (>= 4.2)
omniauth (~> 2.0)
+ opensearch-ruby (3.4.0)
+ faraday (>= 1.0, < 3)
+ multi_json (>= 1.0)
openssl (3.2.0)
orm_adapter (0.5.0)
os (1.1.4)
@@ -591,7 +635,7 @@ GEM
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.8.1)
- rack (2.2.15)
+ rack (3.2.0)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-contrib (2.5.0)
@@ -600,19 +644,20 @@ GEM
rack (>= 2.0.0)
rack-mini-profiler (3.2.0)
rack (>= 1.2.0)
- rack-protection (3.2.0)
+ rack-protection (4.1.1)
base64 (>= 0.1.0)
- rack (~> 2.2, >= 2.2.4)
+ logger (>= 1.6.0)
+ rack (>= 3.0.0, < 4)
rack-proxy (0.7.7)
rack
- rack-session (1.0.2)
- rack (< 3)
+ rack-session (2.1.1)
+ base64 (>= 0.1.0)
+ rack (>= 3.0.0)
rack-test (2.1.0)
rack (>= 1.3)
rack-timeout (0.6.3)
- rackup (1.0.1)
- rack (< 3)
- webrick
+ rackup (2.2.1)
+ rack (>= 3)
rails (7.1.5.2)
actioncable (= 7.1.5.2)
actionmailbox (= 7.1.5.2)
@@ -763,6 +808,9 @@ GEM
parser
scss_lint (0.60.0)
sass (~> 3.5, >= 3.5.5)
+ searchkick (5.5.2)
+ activemodel (>= 7.1)
+ hashie
securerandom (0.4.1)
seed_dump (3.3.1)
activerecord (>= 4)
@@ -809,11 +857,12 @@ GEM
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
- simplecov (0.17.1)
+ simplecov (0.22.0)
docile (~> 1.1)
- json (>= 1.8, < 3)
- simplecov-html (~> 0.10.0)
- simplecov-html (0.10.2)
+ simplecov-html (~> 0.11)
+ simplecov_json_formatter (~> 0.1)
+ simplecov-html (0.13.2)
+ simplecov_json_formatter (0.1.4)
slack-ruby-client (2.5.2)
faraday (>= 2.0)
faraday-mashify
@@ -843,13 +892,17 @@ GEM
telephone_number (1.4.20)
test-prof (1.2.1)
thor (1.4.0)
+ tidewave (0.2.0)
+ fast-mcp (~> 1.5.0)
+ rack (>= 2.0)
+ rails (>= 7.1.0)
tilt (2.3.0)
time_diff (0.3.0)
activesupport
i18n
timeout (0.4.3)
trailblazer-option (0.1.2)
- twilio-ruby (5.77.0)
+ twilio-ruby (7.6.0)
faraday (>= 0.9, < 3.0)
jwt (>= 1.5, < 3.0)
nokogiri (>= 1.6, < 2.0)
@@ -896,7 +949,6 @@ GEM
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
- webrick (1.9.1)
websocket-driver (0.7.7)
base64
websocket-extensions (>= 0.1.0)
@@ -954,6 +1006,7 @@ DEPENDENCIES
facebook-messenger
factory_bot_rails (>= 6.4.3)
faker
+ faraday_middleware-aws-sigv4
fcm
flag_shih_tzu
foreman
@@ -994,6 +1047,7 @@ DEPENDENCIES
omniauth-google-oauth2 (>= 1.1.3)
omniauth-oauth2
omniauth-rails_csrf_protection (~> 1.0, >= 1.0.2)
+ opensearch-ruby
pg
pg_search
pgvector
@@ -1022,6 +1076,7 @@ DEPENDENCIES
ruby_llm-schema
scout_apm
scss_lint
+ searchkick
seed_dump
sentry-rails (>= 5.19.0)
sentry-ruby
@@ -1031,7 +1086,8 @@ DEPENDENCIES
sidekiq (>= 7.3.1)
sidekiq-cron (>= 1.12.0)
sidekiq_alive
- simplecov (= 0.17.1)
+ simplecov (>= 0.21)
+ simplecov_json_formatter
slack-ruby-client (~> 2.5.2)
spring
spring-watcher-listen
@@ -1040,8 +1096,9 @@ DEPENDENCIES
stripe
telephone_number
test-prof
+ tidewave
time_diff
- twilio-ruby (~> 5.66)
+ twilio-ruby
twitty (~> 0.1.5)
tzinfo-data
uglifier
diff --git a/Rakefile b/Rakefile
index e85f91391..591d2c4c6 100644
--- a/Rakefile
+++ b/Rakefile
@@ -4,3 +4,7 @@
require_relative 'config/application'
Rails.application.load_tasks
+
+# Load Enterprise Edition rake tasks if they exist
+enterprise_tasks_path = Rails.root.join('enterprise/lib/tasks.rb').to_s
+require enterprise_tasks_path if File.exist?(enterprise_tasks_path)
diff --git a/VERSION_CWCTL b/VERSION_CWCTL
index 4d9d11cf5..6cb9d3dd0 100644
--- a/VERSION_CWCTL
+++ b/VERSION_CWCTL
@@ -1 +1 @@
-3.4.2
+3.4.3
diff --git a/app/builders/v2/reports/label_summary_builder.rb b/app/builders/v2/reports/label_summary_builder.rb
index caa5a04d8..8b7c21e8e 100644
--- a/app/builders/v2/reports/label_summary_builder.rb
+++ b/app/builders/v2/reports/label_summary_builder.rb
@@ -28,7 +28,7 @@ class V2::Reports::LabelSummaryBuilder < V2::Reports::BaseSummaryBuilder
{
conversation_counts: fetch_conversation_counts(conversation_filter),
- resolved_counts: fetch_resolved_counts(conversation_filter),
+ resolved_counts: fetch_resolved_counts,
resolution_metrics: fetch_metrics(conversation_filter, 'conversation_resolved', use_business_hours),
first_response_metrics: fetch_metrics(conversation_filter, 'first_response', use_business_hours),
reply_metrics: fetch_metrics(conversation_filter, 'reply_time', use_business_hours)
@@ -62,10 +62,21 @@ class V2::Reports::LabelSummaryBuilder < V2::Reports::BaseSummaryBuilder
fetch_counts(conversation_filter)
end
- def fetch_resolved_counts(conversation_filter)
- # since the base query is ActsAsTaggableOn,
- # the status :resolved won't automatically be converted to integer status
- fetch_counts(conversation_filter.merge(status: Conversation.statuses[:resolved]))
+ def fetch_resolved_counts
+ # Count resolution events, not conversations currently in resolved status
+ # Filter by reporting_event.created_at, not conversation.created_at
+ reporting_event_filter = { name: 'conversation_resolved', account_id: account.id }
+ reporting_event_filter[:created_at] = range if range.present?
+
+ ReportingEvent
+ .joins(conversation: { taggings: :tag })
+ .where(
+ reporting_event_filter.merge(
+ taggings: { taggable_type: 'Conversation', context: 'labels' }
+ )
+ )
+ .group('tags.name')
+ .count
end
def fetch_counts(conversation_filter)
@@ -84,9 +95,7 @@ class V2::Reports::LabelSummaryBuilder < V2::Reports::BaseSummaryBuilder
def fetch_metrics(conversation_filter, event_name, use_business_hours)
ReportingEvent
- .joins('INNER JOIN conversations ON reporting_events.conversation_id = conversations.id')
- .joins('INNER JOIN taggings ON taggings.taggable_id = conversations.id')
- .joins('INNER JOIN tags ON taggings.tag_id = tags.id')
+ .joins(conversation: { taggings: :tag })
.where(
conversations: conversation_filter,
name: event_name,
diff --git a/app/builders/v2/reports/timeseries/count_report_builder.rb b/app/builders/v2/reports/timeseries/count_report_builder.rb
index 03a87a6fa..bb3b1250c 100644
--- a/app/builders/v2/reports/timeseries/count_report_builder.rb
+++ b/app/builders/v2/reports/timeseries/count_report_builder.rb
@@ -38,27 +38,34 @@ class V2::Reports::Timeseries::CountReportBuilder < V2::Reports::Timeseries::Bas
end
def scope_for_resolutions_count
- scope.reporting_events.joins(:conversation).select(:conversation_id).where(
+ scope.reporting_events.where(
name: :conversation_resolved,
- conversations: { status: :resolved }, created_at: range
- ).distinct
+ account_id: account.id,
+ created_at: range
+ )
end
def scope_for_bot_resolutions_count
- scope.reporting_events.joins(:conversation).select(:conversation_id).where(
+ scope.reporting_events.where(
name: :conversation_bot_resolved,
- conversations: { status: :resolved }, created_at: range
- ).distinct
+ account_id: account.id,
+ created_at: range
+ )
end
def scope_for_bot_handoffs_count
scope.reporting_events.joins(:conversation).select(:conversation_id).where(
name: :conversation_bot_handoff,
+ account_id: account.id,
created_at: range
).distinct
end
def grouped_count
+ # IMPORTANT: time_zone parameter affects both data grouping AND output timestamps
+ # It converts timestamps to the target timezone before grouping, which means
+ # the same event can fall into different day buckets depending on timezone
+ # Example: 2024-01-15 00:00 UTC becomes 2024-01-14 16:00 PST (falls on different day)
@grouped_values = object_scope.group_by_period(
group_by,
:created_at,
diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb
index 78b4b9e2f..4750e3b4a 100644
--- a/app/controllers/api/v1/accounts/inboxes_controller.rb
+++ b/app/controllers/api/v1/accounts/inboxes_controller.rb
@@ -70,11 +70,9 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
end
def sync_templates
- unless @inbox.channel.is_a?(Channel::Whatsapp)
- return render status: :unprocessable_entity, json: { error: 'Template sync is only available for WhatsApp channels' }
- end
+ return render status: :unprocessable_entity, json: { error: 'Template sync is only available for WhatsApp channels' } unless whatsapp_channel?
- Channels::Whatsapp::TemplatesSyncJob.perform_later(@inbox.channel)
+ trigger_template_sync
render status: :ok, json: { message: 'Template sync initiated successfully' }
rescue StandardError => e
render status: :internal_server_error, json: { error: e.message }
@@ -185,6 +183,18 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
[]
end
end
+
+ def whatsapp_channel?
+ @inbox.whatsapp? || (@inbox.twilio? && @inbox.channel.whatsapp?)
+ end
+
+ def trigger_template_sync
+ if @inbox.whatsapp?
+ Channels::Whatsapp::TemplatesSyncJob.perform_later(@inbox.channel)
+ elsif @inbox.twilio? && @inbox.channel.whatsapp?
+ Channels::Twilio::TemplatesSyncJob.perform_later(@inbox.channel)
+ end
+ end
end
Api::V1::Accounts::InboxesController.prepend_mod_with('Api::V1::Accounts::InboxesController')
diff --git a/app/controllers/api/v1/widget/configs_controller.rb b/app/controllers/api/v1/widget/configs_controller.rb
index ac88c595a..ecbddd905 100644
--- a/app/controllers/api/v1/widget/configs_controller.rb
+++ b/app/controllers/api/v1/widget/configs_controller.rb
@@ -9,7 +9,7 @@ class Api::V1::Widget::ConfigsController < Api::V1::Widget::BaseController
private
def set_global_config
- @global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL')
+ @global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL', 'INSTALLATION_NAME')
end
def set_contact
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 4a2df5ee5..d81b4c9da 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -66,7 +66,7 @@ class DashboardController < ActionController::Base
ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'),
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''),
INSTAGRAM_APP_ID: GlobalConfigService.load('INSTAGRAM_APP_ID', ''),
- FACEBOOK_API_VERSION: GlobalConfigService.load('FACEBOOK_API_VERSION', 'v17.0'),
+ FACEBOOK_API_VERSION: GlobalConfigService.load('FACEBOOK_API_VERSION', 'v18.0'),
WHATSAPP_APP_ID: GlobalConfigService.load('WHATSAPP_APP_ID', ''),
WHATSAPP_CONFIGURATION_ID: GlobalConfigService.load('WHATSAPP_CONFIGURATION_ID', ''),
IS_ENTERPRISE: ChatwootApp.enterprise?,
diff --git a/app/controllers/public/api/v1/portals/base_controller.rb b/app/controllers/public/api/v1/portals/base_controller.rb
index 66b052b1e..46158bce9 100644
--- a/app/controllers/public/api/v1/portals/base_controller.rb
+++ b/app/controllers/public/api/v1/portals/base_controller.rb
@@ -58,6 +58,6 @@ class Public::Api::V1::Portals::BaseController < PublicController
end
def set_global_config
- @global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'BRAND_URL')
+ @global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'BRAND_URL', 'INSTALLATION_NAME')
end
end
diff --git a/app/controllers/survey/responses_controller.rb b/app/controllers/survey/responses_controller.rb
index 8bbd0fe88..afcb3f4c0 100644
--- a/app/controllers/survey/responses_controller.rb
+++ b/app/controllers/survey/responses_controller.rb
@@ -5,6 +5,6 @@ class Survey::ResponsesController < ActionController::Base
private
def set_global_config
- @global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL')
+ @global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL', 'INSTALLATION_NAME')
end
end
diff --git a/app/controllers/widgets_controller.rb b/app/controllers/widgets_controller.rb
index 70e4c967b..4be690ffe 100644
--- a/app/controllers/widgets_controller.rb
+++ b/app/controllers/widgets_controller.rb
@@ -14,7 +14,7 @@ class WidgetsController < ActionController::Base
private
def set_global_config
- @global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL', 'DIRECT_UPLOADS_ENABLED')
+ @global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL', 'DIRECT_UPLOADS_ENABLED', 'INSTALLATION_NAME')
end
def set_web_widget
diff --git a/app/helpers/portal_helper.rb b/app/helpers/portal_helper.rb
index 3ed303556..e98f0a72d 100644
--- a/app/helpers/portal_helper.rb
+++ b/app/helpers/portal_helper.rb
@@ -74,6 +74,17 @@ module PortalHelper
end
end
+ def generate_portal_brand_url(brand_url, referer)
+ url = URI.parse(brand_url.to_s)
+ query_params = Rack::Utils.parse_query(url.query)
+ query_params['utm_medium'] = 'helpcenter'
+ query_params['utm_campaign'] = 'branding'
+ query_params['utm_source'] = URI.parse(referer).host if referer.present? && referer.match?(URI::DEFAULT_PARSER.make_regexp)
+
+ url.query = query_params.to_query
+ url.to_s
+ end
+
def render_category_content(content)
ChatwootMarkdownRenderer.new(content).render_markdown_to_plain_text
end
diff --git a/app/helpers/report_helper.rb b/app/helpers/report_helper.rb
index 99f3fd36b..09a84b110 100644
--- a/app/helpers/report_helper.rb
+++ b/app/helpers/report_helper.rb
@@ -53,13 +53,13 @@ module ReportHelper
end
def resolutions
- scope.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_resolved,
- conversations: { status: :resolved }, created_at: range).distinct
+ scope.reporting_events.where(account_id: account.id, name: :conversation_resolved,
+ created_at: range)
end
def bot_resolutions
- scope.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_bot_resolved,
- conversations: { status: :resolved }, created_at: range).distinct
+ scope.reporting_events.where(account_id: account.id, name: :conversation_bot_resolved,
+ created_at: range)
end
def bot_handoffs
diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/AssignmentCard/AssignmentCard.story.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/AssignmentCard/AssignmentCard.story.vue
new file mode 100644
index 000000000..e35be8155
--- /dev/null
+++ b/app/javascript/dashboard/components-next/AssignmentPolicy/AssignmentCard/AssignmentCard.story.vue
@@ -0,0 +1,63 @@
+
+
+
+ {{ description }}{{ title }}
+
+
+
+