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 @@ + + + diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/AssignmentCard/AssignmentCard.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/AssignmentCard/AssignmentCard.vue new file mode 100644 index 000000000..1e477eafe --- /dev/null +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/AssignmentCard/AssignmentCard.vue @@ -0,0 +1,49 @@ + + + diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactListHeaderWrapper.vue b/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactListHeaderWrapper.vue index ea5c63ebc..0b4880264 100644 --- a/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactListHeaderWrapper.vue +++ b/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactListHeaderWrapper.vue @@ -62,6 +62,7 @@ const segmentsQuery = ref({}); const appliedFilters = useMapGetter('contacts/getAppliedContactFiltersV4'); const contactAttributes = useMapGetter('attributes/getContactAttributes'); +const labels = useMapGetter('labels/getLabels'); const hasActiveSegments = computed( () => props.activeSegment && props.segmentsId !== 0 ); @@ -215,6 +216,7 @@ const setParamsForEditSegmentModal = () => { countries, filterTypes: contactFilterItems, allCustomAttributes: useSnakeCase(contactAttributes.value), + labels: labels.value || [], }; }; diff --git a/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue b/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue index 6f2178408..773ebe315 100644 --- a/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue +++ b/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue @@ -11,12 +11,14 @@ import { extractTextFromMarkdown } from 'dashboard/helper/editorHelper'; import Button from 'dashboard/components-next/button/Button.vue'; import WhatsAppOptions from './WhatsAppOptions.vue'; +import ContentTemplateSelector from './ContentTemplateSelector.vue'; const props = defineProps({ attachedFiles: { type: Array, default: () => [] }, isWhatsappInbox: { type: Boolean, default: false }, isEmailOrWebWidgetInbox: { type: Boolean, default: false }, isTwilioSmsInbox: { type: Boolean, default: false }, + isTwilioWhatsAppInbox: { type: Boolean, default: false }, messageTemplates: { type: Array, default: () => [] }, channelType: { type: String, default: '' }, isLoading: { type: Boolean, default: false }, @@ -32,6 +34,7 @@ const emit = defineEmits([ 'discard', 'sendMessage', 'sendWhatsappMessage', + 'sendTwilioMessage', 'insertEmoji', 'addSignature', 'removeSignature', @@ -63,6 +66,20 @@ const sendWithSignature = computed(() => { return fetchSignatureFlagFromUISettings(props.channelType); }); +const showTwilioContentTemplates = computed(() => { + return props.isTwilioWhatsAppInbox && props.inboxId; +}); + +const shouldShowEmojiButton = computed(() => { + return ( + !props.isWhatsappInbox && !props.isTwilioWhatsAppInbox && !props.hasNoInbox + ); +}); + +const isRegularMessageMode = computed(() => { + return !props.isWhatsappInbox && !props.isTwilioWhatsAppInbox; +}); + const setSignature = () => { if (signatureToApply.value) { if (sendWithSignature.value) { @@ -125,7 +142,7 @@ const keyboardEvents = { action: () => { if ( isEditorHotKeyEnabled('enter') && - !props.isWhatsappInbox && + isRegularMessageMode.value && !props.isDropdownActive ) { emit('sendMessage'); @@ -136,7 +153,7 @@ const keyboardEvents = { action: () => { if ( isEditorHotKeyEnabled('cmd_enter') && - !props.isWhatsappInbox && + isRegularMessageMode.value && !props.isDropdownActive ) { emit('sendMessage'); @@ -158,8 +175,13 @@ useKeyboardEvents(keyboardEvents); :message-templates="messageTemplates" @send-message="emit('sendWhatsappMessage', $event)" /> +
@@ -172,7 +194,7 @@ useKeyboardEvents(keyboardEvents); />
@@ -199,7 +221,7 @@ useKeyboardEvents(keyboardEvents); />