diff --git a/.gitignore b/.gitignore index c64fb5c1b..bb0df62a8 100644 --- a/.gitignore +++ b/.gitignore @@ -94,3 +94,4 @@ yarn-debug.log* .vscode .claude/settings.local.json .cursor +CLAUDE.local.md diff --git a/AGENTS.md b/AGENTS.md index ad374799c..e3b022a2e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,4 +55,21 @@ ## Ruby Best Practices -- Use compact `module/class` definitions; avoid nested styles \ No newline at end of file +- Use compact `module/class` definitions; avoid nested styles + +## Enterprise Edition Notes + +- Chatwoot has an Enterprise overlay under `enterprise/` that extends/overrides OSS code. +- When you add or modify core functionality, always check for corresponding files in `enterprise/` and keep behavior compatible. +- Follow the Enterprise development practices documented here: + - https://chatwoot.help/hc/handbook/articles/developing-enterprise-edition-features-38 + +Practical checklist for any change impacting core logic or public APIs +- Search for related files in both trees before editing (e.g., `rg -n "FooService|ControllerName|ModelName" app enterprise`). +- If adding new endpoints, services, or models, consider whether Enterprise needs: + - An override (e.g., `enterprise/app/...`), or + - An extension point (e.g., `prepend_mod_with`, hooks, configuration) to avoid hard forks. +- Avoid hardcoding instance- or plan-specific behavior in OSS; prefer configuration, feature flags, or extension points consumed by Enterprise. +- Keep request/response contracts stable across OSS and Enterprise; update both sets of routes/controllers when introducing new APIs. +- When renaming/moving shared code, mirror the change in `enterprise/` to prevent drift. +- Tests: Add Enterprise-specific specs under `spec/enterprise`, mirroring OSS spec layout where applicable. diff --git a/Gemfile b/Gemfile index ac2acebf0..112b2b8af 100644 --- a/Gemfile +++ b/Gemfile @@ -90,7 +90,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' @@ -109,7 +109,7 @@ gem 'google-cloud-translate-v3', '>= 0.7.0' ##-- apm and error monitoring ---# # loaded only when environment variables are set. # ref application.rb -gem 'ddtrace', require: false +gem 'datadog', '~> 2.0', require: false gem 'elastic-apm', require: false gem 'newrelic_rpm', require: false gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false @@ -180,7 +180,10 @@ gem 'reverse_markdown' gem 'iso-639' gem 'ruby-openai' -gem 'ai-agents', '>= 0.2.1' +gem 'ai-agents', '>= 0.4.3' + +# TODO: Move this gem as a dependency of ai-agents +gem 'ruby_llm-schema' gem 'shopify_api' @@ -210,6 +213,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 diff --git a/Gemfile.lock b/Gemfile.lock index 343db1861..dbe123fc2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -25,35 +25,35 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (7.1.5.1) - actionpack (= 7.1.5.1) - activesupport (= 7.1.5.1) + actioncable (7.1.5.2) + actionpack (= 7.1.5.2) + activesupport (= 7.1.5.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.1.5.1) - actionpack (= 7.1.5.1) - activejob (= 7.1.5.1) - activerecord (= 7.1.5.1) - activestorage (= 7.1.5.1) - activesupport (= 7.1.5.1) + actionmailbox (7.1.5.2) + actionpack (= 7.1.5.2) + activejob (= 7.1.5.2) + activerecord (= 7.1.5.2) + activestorage (= 7.1.5.2) + activesupport (= 7.1.5.2) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.1.5.1) - actionpack (= 7.1.5.1) - actionview (= 7.1.5.1) - activejob (= 7.1.5.1) - activesupport (= 7.1.5.1) + actionmailer (7.1.5.2) + actionpack (= 7.1.5.2) + actionview (= 7.1.5.2) + activejob (= 7.1.5.2) + activesupport (= 7.1.5.2) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.2) - actionpack (7.1.5.1) - actionview (= 7.1.5.1) - activesupport (= 7.1.5.1) + actionpack (7.1.5.2) + actionview (= 7.1.5.2) + activesupport (= 7.1.5.2) nokogiri (>= 1.8.5) racc rack (>= 2.2.4) @@ -61,38 +61,38 @@ GEM rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - actiontext (7.1.5.1) - actionpack (= 7.1.5.1) - activerecord (= 7.1.5.1) - activestorage (= 7.1.5.1) - activesupport (= 7.1.5.1) + actiontext (7.1.5.2) + actionpack (= 7.1.5.2) + activerecord (= 7.1.5.2) + activestorage (= 7.1.5.2) + activesupport (= 7.1.5.2) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.1.5.1) - activesupport (= 7.1.5.1) + actionview (7.1.5.2) + activesupport (= 7.1.5.2) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) active_record_query_trace (1.8) - activejob (7.1.5.1) - activesupport (= 7.1.5.1) + activejob (7.1.5.2) + activesupport (= 7.1.5.2) globalid (>= 0.3.6) - activemodel (7.1.5.1) - activesupport (= 7.1.5.1) - activerecord (7.1.5.1) - activemodel (= 7.1.5.1) - activesupport (= 7.1.5.1) + activemodel (7.1.5.2) + activesupport (= 7.1.5.2) + activerecord (7.1.5.2) + activemodel (= 7.1.5.2) + activesupport (= 7.1.5.2) timeout (>= 0.4.0) activerecord-import (2.1.0) activerecord (>= 4.2) - activestorage (7.1.5.1) - actionpack (= 7.1.5.1) - activejob (= 7.1.5.1) - activerecord (= 7.1.5.1) - activesupport (= 7.1.5.1) + activestorage (7.1.5.2) + actionpack (= 7.1.5.2) + activejob (= 7.1.5.2) + activerecord (= 7.1.5.2) + activesupport (= 7.1.5.2) marcel (~> 1.0) - activesupport (7.1.5.1) + activesupport (7.1.5.2) base64 benchmark (>= 0.3) bigdecimal @@ -126,7 +126,7 @@ GEM jbuilder (~> 2) rails (>= 4.2, < 7.2) selectize-rails (~> 0.6) - ai-agents (0.2.1) + ai-agents (0.4.3) ruby_llm (~> 1.3) annotate (3.2.0) activerecord (>= 3.2, < 8.0) @@ -155,10 +155,10 @@ GEM barnes (0.0.9) multi_json (~> 1) statsd-ruby (~> 1.1) - base64 (0.2.0) + base64 (0.3.0) bcrypt (3.1.20) - benchmark (0.4.0) - bigdecimal (3.1.9) + benchmark (0.4.1) + bigdecimal (3.2.2) bindex (0.8.1) bootsnap (1.16.0) msgpack (~> 1.2) @@ -194,10 +194,14 @@ GEM activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - date (3.4.1) - ddtrace (0.48.0) - ffi (~> 1.0) + datadog (2.19.0) + datadog-ruby_core_source (~> 3.4, >= 3.4.1) + libdatadog (~> 18.1.0.1.0) + libddwaf (~> 1.24.1.0.3) + logger msgpack + datadog-ruby_core_source (3.4.1) + date (3.4.1) debug (1.8.0) irb (>= 1.5.0) reline (>= 0.3.1) @@ -226,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) @@ -248,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) @@ -257,13 +292,20 @@ 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) + 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) @@ -417,7 +459,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) @@ -444,6 +486,16 @@ GEM logger (~> 1.6) letter_opener (1.10.0) launchy (>= 2.2, < 4) + libdatadog (18.1.0.1.0) + libdatadog (18.1.0.1.0-x86_64-linux) + libddwaf (1.24.1.0.3) + ffi (~> 1.0) + libddwaf (1.24.1.0.3-arm64-darwin) + ffi (~> 1.0) + libddwaf (1.24.1.0.3-x86_64-darwin) + ffi (~> 1.0) + libddwaf (1.24.1.0.3-x86_64-linux) + ffi (~> 1.0) line-bot-api (1.28.0) lint_roller (1.1.0) liquid (5.4.0) @@ -489,7 +541,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) @@ -580,7 +632,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) @@ -589,33 +641,34 @@ 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 - rails (7.1.5.1) - actioncable (= 7.1.5.1) - actionmailbox (= 7.1.5.1) - actionmailer (= 7.1.5.1) - actionpack (= 7.1.5.1) - actiontext (= 7.1.5.1) - actionview (= 7.1.5.1) - activejob (= 7.1.5.1) - activemodel (= 7.1.5.1) - activerecord (= 7.1.5.1) - activestorage (= 7.1.5.1) - activesupport (= 7.1.5.1) + rackup (2.2.1) + rack (>= 3) + rails (7.1.5.2) + actioncable (= 7.1.5.2) + actionmailbox (= 7.1.5.2) + actionmailer (= 7.1.5.2) + actionpack (= 7.1.5.2) + actiontext (= 7.1.5.2) + actionview (= 7.1.5.2) + activejob (= 7.1.5.2) + activemodel (= 7.1.5.2) + activerecord (= 7.1.5.2) + activestorage (= 7.1.5.2) + activesupport (= 7.1.5.2) bundler (>= 1.15.0) - railties (= 7.1.5.1) + railties (= 7.1.5.2) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -623,9 +676,9 @@ GEM rails-html-sanitizer (1.6.1) loofah (~> 2.21) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - railties (7.1.5.1) - actionpack (= 7.1.5.1) - activesupport (= 7.1.5.1) + railties (7.1.5.2) + actionpack (= 7.1.5.2) + activesupport (= 7.1.5.2) irb rackup (>= 1.0.0) rake (>= 12.2) @@ -723,7 +776,7 @@ GEM ruby2ruby (2.5.0) ruby_parser (~> 3.1) sexp_processor (~> 4.6) - ruby_llm (1.3.1) + ruby_llm (1.5.1) base64 event_stream_parser (~> 1) faraday (>= 1.10.0) @@ -732,6 +785,7 @@ GEM faraday-retry (>= 1) marcel (~> 1.0) zeitwerk (~> 2) + ruby_llm-schema (0.1.0) ruby_parser (3.20.0) sexp_processor (~> 4.16) sass (3.7.4) @@ -834,13 +888,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) @@ -887,7 +945,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) @@ -916,7 +973,7 @@ DEPENDENCIES administrate (>= 0.20.1) administrate-field-active_storage (>= 1.0.3) administrate-field-belongs_to_search (>= 0.9.0) - ai-agents (>= 0.2.1) + ai-agents (>= 0.4.3) annotate attr_extras audited (~> 5.4, >= 5.4.1) @@ -933,7 +990,7 @@ DEPENDENCIES commonmarker csv-safe database_cleaner - ddtrace + datadog (~> 2.0) debug (~> 1.8) devise (>= 4.9.4) devise-secure_password! @@ -1011,6 +1068,7 @@ DEPENDENCIES rubocop-rails rubocop-rspec ruby-openai + ruby_llm-schema scout_apm scss_lint seed_dump @@ -1031,8 +1089,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/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/controllers/api/v1/accounts/assignment_policies/inboxes_controller.rb b/app/controllers/api/v1/accounts/assignment_policies/inboxes_controller.rb new file mode 100644 index 000000000..ac1d0a712 --- /dev/null +++ b/app/controllers/api/v1/accounts/assignment_policies/inboxes_controller.rb @@ -0,0 +1,20 @@ +class Api::V1::Accounts::AssignmentPolicies::InboxesController < Api::V1::Accounts::BaseController + before_action :fetch_assignment_policy + before_action -> { check_authorization(AssignmentPolicy) } + + def index + @inboxes = @assignment_policy.inboxes + end + + private + + def fetch_assignment_policy + @assignment_policy = Current.account.assignment_policies.find( + params[:assignment_policy_id] + ) + end + + def permitted_params + params.permit(:assignment_policy_id) + end +end diff --git a/app/controllers/api/v1/accounts/assignment_policies_controller.rb b/app/controllers/api/v1/accounts/assignment_policies_controller.rb new file mode 100644 index 000000000..1807d6afb --- /dev/null +++ b/app/controllers/api/v1/accounts/assignment_policies_controller.rb @@ -0,0 +1,36 @@ +class Api::V1::Accounts::AssignmentPoliciesController < Api::V1::Accounts::BaseController + before_action :fetch_assignment_policy, only: [:show, :update, :destroy] + before_action :check_authorization + + def index + @assignment_policies = Current.account.assignment_policies + end + + def show; end + + def create + @assignment_policy = Current.account.assignment_policies.create!(assignment_policy_params) + end + + def update + @assignment_policy.update!(assignment_policy_params) + end + + def destroy + @assignment_policy.destroy! + head :ok + end + + private + + def fetch_assignment_policy + @assignment_policy = Current.account.assignment_policies.find(params[:id]) + end + + def assignment_policy_params + params.require(:assignment_policy).permit( + :name, :description, :assignment_order, :conversation_priority, + :fair_distribution_limit, :fair_distribution_window, :enabled + ) + end +end diff --git a/app/controllers/api/v1/accounts/inboxes/assignment_policies_controller.rb b/app/controllers/api/v1/accounts/inboxes/assignment_policies_controller.rb new file mode 100644 index 000000000..cf52951a5 --- /dev/null +++ b/app/controllers/api/v1/accounts/inboxes/assignment_policies_controller.rb @@ -0,0 +1,46 @@ +class Api::V1::Accounts::Inboxes::AssignmentPoliciesController < Api::V1::Accounts::BaseController + before_action :fetch_inbox + before_action :fetch_assignment_policy, only: [:create] + before_action -> { check_authorization(AssignmentPolicy) } + before_action :validate_assignment_policy, only: [:show, :destroy] + + def show + @assignment_policy = @inbox.assignment_policy + end + + def create + # There should be only one assignment policy for an inbox. + # If there is a new request to add an assignment policy, we will + # delete the old one and attach the new policy + remove_inbox_assignment_policy + @inbox_assignment_policy = @inbox.create_inbox_assignment_policy!(assignment_policy: @assignment_policy) + @assignment_policy = @inbox.assignment_policy + end + + def destroy + remove_inbox_assignment_policy + head :ok + end + + private + + def remove_inbox_assignment_policy + @inbox.inbox_assignment_policy&.destroy + end + + def fetch_inbox + @inbox = Current.account.inboxes.find(permitted_params[:inbox_id]) + end + + def fetch_assignment_policy + @assignment_policy = Current.account.assignment_policies.find(permitted_params[:assignment_policy_id]) + end + + def permitted_params + params.permit(:assignment_policy_id, :inbox_id) + end + + def validate_assignment_policy + return render_not_found_error(I18n.t('errors.assignment_policy.not_found')) unless @inbox.assignment_policy + end +end diff --git a/app/helpers/reporting_event_helper.rb b/app/helpers/reporting_event_helper.rb index e08b5691c..f0a419cc8 100644 --- a/app/helpers/reporting_event_helper.rb +++ b/app/helpers/reporting_event_helper.rb @@ -18,12 +18,25 @@ module ReportingEventHelper end def last_non_human_activity(conversation) - # check if a handoff event already exists - handoff_event = ReportingEvent.where(conversation_id: conversation.id, name: 'conversation_bot_handoff').last + # Try to get either a handoff or reopened event first + # These will always take precedence over any other activity + # Also, any of these events can happen at any time in the course of a conversation lifecycle. + # So we pick the latest event + event = ReportingEvent.where( + conversation_id: conversation.id, + name: %w[conversation_bot_handoff conversation_opened] + ).order(event_end_time: :desc).first - # if a handoff exists, last non human activity is when the handoff ended, - # otherwise it's when the conversation was created - handoff_event&.event_end_time || conversation.created_at + return event.event_end_time if event&.event_end_time + + # Fallback to bot resolved event + # Because this will be closest to the most accurate activity instead of conversation.created_at + bot_event = ReportingEvent.where(conversation_id: conversation.id, name: 'conversation_bot_resolved').last + + return bot_event.event_end_time if bot_event&.event_end_time + + # If no events found, return conversation creation time + conversation.created_at end private diff --git a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignForm.vue b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignForm.vue index df76ae901..0babd11ec 100644 --- a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignForm.vue +++ b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignForm.vue @@ -9,6 +9,7 @@ import Input from 'dashboard/components-next/input/Input.vue'; import Button from 'dashboard/components-next/button/Button.vue'; import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue'; import TagMultiSelectComboBox from 'dashboard/components-next/combobox/TagMultiSelectComboBox.vue'; +import WhatsAppTemplateParser from 'dashboard/components-next/whatsapp/WhatsAppTemplateParser.vue'; const emit = defineEmits(['submit', 'cancel']); @@ -18,7 +19,9 @@ const formState = { uiFlags: useMapGetter('campaigns/getUIFlags'), labels: useMapGetter('labels/getLabels'), inboxes: useMapGetter('inboxes/getWhatsAppInboxes'), - getWhatsAppTemplates: useMapGetter('inboxes/getWhatsAppTemplates'), + getFilteredWhatsAppTemplates: useMapGetter( + 'inboxes/getFilteredWhatsAppTemplates' + ), }; const initialState = { @@ -30,7 +33,7 @@ const initialState = { }; const state = reactive({ ...initialState }); -const processedParams = ref({}); +const templateParserRef = ref(null); const rules = { title: { required, minLength: minLength(1) }, @@ -67,7 +70,7 @@ const inboxOptions = computed(() => const templateOptions = computed(() => { if (!state.inboxId) return []; - const templates = formState.getWhatsAppTemplates.value(state.inboxId); + const templates = formState.getFilteredWhatsAppTemplates.value(state.inboxId); return templates.map(template => { // Create a more user-friendly label from template name const friendlyName = template.name @@ -88,26 +91,6 @@ const selectedTemplate = computed(() => { ?.template; }); -const templateString = computed(() => { - if (!selectedTemplate.value) return ''; - try { - return ( - selectedTemplate.value.components?.find( - component => component.type === 'BODY' - )?.text || '' - ); - } catch (error) { - return ''; - } -}); - -const processedString = computed(() => { - if (!templateString.value) return ''; - return templateString.value.replace(/{{([^}]+)}}/g, (match, variable) => { - return processedParams.value[variable] || `{{${variable}}}`; - }); -}); - const getErrorMessage = (field, errorKey) => { const baseKey = 'CAMPAIGN.WHATSAPP.CREATE.FORM'; return v$.value[field].$error ? t(`${baseKey}.${errorKey}.ERROR`) : ''; @@ -122,8 +105,7 @@ const formErrors = computed(() => ({ })); const hasRequiredTemplateParams = computed(() => { - const params = Object.values(processedParams.value); - return params.length === 0 || params.every(param => param.trim() !== ''); + return templateParserRef.value?.v$?.$invalid === false || true; }); const isSubmitDisabled = computed( @@ -135,32 +117,18 @@ const formatToUTCString = localDateTime => const resetState = () => { Object.assign(state, initialState); - processedParams.value = {}; v$.value.$reset(); }; const handleCancel = () => emit('cancel'); -const generateVariables = () => { - const matchedVariables = templateString.value.match(/{{([^}]+)}}/g); - if (!matchedVariables) { - processedParams.value = {}; - return; - } - - const finalVars = matchedVariables.map(match => match.replace(/{{|}}/g, '')); - processedParams.value = finalVars.reduce((acc, variable) => { - acc[variable] = processedParams.value[variable] || ''; - return acc; - }, {}); -}; - const prepareCampaignDetails = () => { // Find the selected template to get its content const currentTemplate = selectedTemplate.value; + const parserData = templateParserRef.value; // Extract template content - this should be the template message body - const templateContent = templateString.value; + const templateContent = parserData?.renderedTemplate || ''; // Prepare template_params object with the same structure as used in contacts const templateParams = { @@ -168,7 +136,7 @@ const prepareCampaignDetails = () => { namespace: currentTemplate?.namespace || '', category: currentTemplate?.category || 'UTILITY', language: currentTemplate?.language || 'en_US', - processed_params: processedParams.value, + processed_params: parserData?.processedParams || {}, }; return { @@ -198,15 +166,6 @@ watch( () => state.inboxId, () => { state.templateId = null; - processedParams.value = {}; - } -); - -// Generate variables when template changes -watch( - () => state.templateId, - () => { - generateVariables(); } ); @@ -254,62 +213,12 @@ watch(

- -
+ -
-

- {{ selectedTemplate.name }} -

- - {{ t('CAMPAIGN.WHATSAPP.CREATE.FORM.TEMPLATE.LANGUAGE') }}: - {{ selectedTemplate.language || 'en' }} - -
- -
-
-
- {{ processedString }} -
-
-
- -
- {{ t('CAMPAIGN.WHATSAPP.CREATE.FORM.TEMPLATE.CATEGORY') }}: - {{ selectedTemplate.category || 'UTILITY' }} -
-
- - -
- -
-
- -
-
-
+ ref="templateParserRef" + :template="selectedTemplate" + />