diff --git a/.rubocop.yml b/.rubocop.yml
index e30a71ee9..ea688792b 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -23,7 +23,7 @@ Metrics/MethodLength:
- 'enterprise/lib/captain/agent.rb'
RSpec/ExampleLength:
- Max: 25
+ Max: 50
Style/Documentation:
Enabled: false
@@ -336,4 +336,4 @@ FactoryBot/RedundantFactoryOption:
Enabled: false
FactoryBot/FactoryAssociationWithStrategy:
- Enabled: false
\ No newline at end of file
+ Enabled: false
diff --git a/app/builders/messages/message_builder.rb b/app/builders/messages/message_builder.rb
index 86bcee54e..12a74ed9c 100644
--- a/app/builders/messages/message_builder.rb
+++ b/app/builders/messages/message_builder.rb
@@ -178,7 +178,13 @@ class Messages::MessageBuilder
email_attributes = ensure_indifferent_access(@message.content_attributes[:email] || {})
normalized_content = normalize_email_body(@message.content)
- email_attributes[:html_content] = build_html_content(normalized_content)
+ # Use custom HTML content if provided, otherwise generate from message content
+ email_attributes[:html_content] = if custom_email_content_provided?
+ build_custom_html_content
+ else
+ build_html_content(normalized_content)
+ end
+
email_attributes[:text_content] = build_text_content(normalized_content)
email_attributes
end
@@ -213,4 +219,17 @@ class Messages::MessageBuilder
ChatwootMarkdownRenderer.new(content).render_message.to_s
end
+
+ def custom_email_content_provided?
+ @params[:email_html_content].present?
+ end
+
+ def build_custom_html_content
+ html_content = ensure_indifferent_access(@message.content_attributes.dig(:email, :html_content) || {})
+
+ html_content[:full] = @params[:email_html_content]
+ html_content[:reply] = @params[:email_html_content]
+
+ html_content
+ end
end
diff --git a/app/builders/v2/reports/base_summary_builder.rb b/app/builders/v2/reports/base_summary_builder.rb
index 4de65926d..d4a9e7c0b 100644
--- a/app/builders/v2/reports/base_summary_builder.rb
+++ b/app/builders/v2/reports/base_summary_builder.rb
@@ -10,10 +10,28 @@ class V2::Reports::BaseSummaryBuilder
def load_data
@conversations_count = fetch_conversations_count
- @resolved_count = fetch_resolved_count
- @avg_resolution_time = fetch_average_time('conversation_resolved')
- @avg_first_response_time = fetch_average_time('first_response')
- @avg_reply_time = fetch_average_time('reply_time')
+ load_reporting_events_data
+ end
+
+ def load_reporting_events_data
+ # Extract the column name for indexing (e.g., 'conversations.team_id' -> 'team_id')
+ index_key = group_by_key.to_s.split('.').last
+
+ results = reporting_events
+ .select(
+ "#{group_by_key} as #{index_key}",
+ "COUNT(CASE WHEN name = 'conversation_resolved' THEN 1 END) as resolved_count",
+ "AVG(CASE WHEN name = 'conversation_resolved' THEN #{average_value_key} END) as avg_resolution_time",
+ "AVG(CASE WHEN name = 'first_response' THEN #{average_value_key} END) as avg_first_response_time",
+ "AVG(CASE WHEN name = 'reply_time' THEN #{average_value_key} END) as avg_reply_time"
+ )
+ .group(group_by_key)
+ .index_by { |record| record.public_send(index_key) }
+
+ @resolved_count = results.transform_values(&:resolved_count)
+ @avg_resolution_time = results.transform_values(&:avg_resolution_time)
+ @avg_first_response_time = results.transform_values(&:avg_first_response_time)
+ @avg_reply_time = results.transform_values(&:avg_reply_time)
end
def reporting_events
@@ -24,14 +42,6 @@ class V2::Reports::BaseSummaryBuilder
# Override this method
end
- def fetch_average_time(event_name)
- get_grouped_average(reporting_events.where(name: event_name))
- end
-
- def fetch_resolved_count
- reporting_events.where(name: 'conversation_resolved').group(group_by_key).count
- end
-
def group_by_key
# Override this method
end
@@ -40,10 +50,6 @@ class V2::Reports::BaseSummaryBuilder
# Override this method
end
- def get_grouped_average(events)
- events.group(group_by_key).average(average_value_key)
- end
-
def average_value_key
ActiveModel::Type::Boolean.new.cast(params[:business_hours]).present? ? :value_in_business_hours : :value
end
diff --git a/app/builders/v2/reports/inbox_summary_builder.rb b/app/builders/v2/reports/inbox_summary_builder.rb
index e27385856..935afeb82 100644
--- a/app/builders/v2/reports/inbox_summary_builder.rb
+++ b/app/builders/v2/reports/inbox_summary_builder.rb
@@ -13,10 +13,7 @@ class V2::Reports::InboxSummaryBuilder < V2::Reports::BaseSummaryBuilder
def load_data
@conversations_count = fetch_conversations_count
- @resolved_count = fetch_resolved_count
- @avg_resolution_time = fetch_average_time('conversation_resolved')
- @avg_first_response_time = fetch_average_time('first_response')
- @avg_reply_time = fetch_average_time('reply_time')
+ load_reporting_events_data
end
def fetch_conversations_count
diff --git a/app/controllers/api/v1/accounts/conversations/base_controller.rb b/app/controllers/api/v1/accounts/conversations/base_controller.rb
index 500c7772f..223530e27 100644
--- a/app/controllers/api/v1/accounts/conversations/base_controller.rb
+++ b/app/controllers/api/v1/accounts/conversations/base_controller.rb
@@ -5,6 +5,6 @@ class Api::V1::Accounts::Conversations::BaseController < Api::V1::Accounts::Base
def conversation
@conversation ||= Current.account.conversations.find_by!(display_id: params[:conversation_id])
- authorize @conversation.inbox, :show?
+ authorize @conversation, :show?
end
end
diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb
index e27869d82..4301eaa4a 100644
--- a/app/controllers/api/v1/accounts/conversations_controller.rb
+++ b/app/controllers/api/v1/accounts/conversations_controller.rb
@@ -160,7 +160,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
def conversation
@conversation ||= Current.account.conversations.find_by!(display_id: params[:id])
- authorize @conversation.inbox, :show?
+ authorize @conversation, :show?
end
def inbox
diff --git a/app/controllers/api/v1/accounts/integrations/dyte_controller.rb b/app/controllers/api/v1/accounts/integrations/dyte_controller.rb
index c5f795d34..845caab5e 100644
--- a/app/controllers/api/v1/accounts/integrations/dyte_controller.rb
+++ b/app/controllers/api/v1/accounts/integrations/dyte_controller.rb
@@ -22,7 +22,7 @@ class Api::V1::Accounts::Integrations::DyteController < Api::V1::Accounts::BaseC
private
def authorize_request
- authorize @conversation.inbox, :show?
+ authorize @conversation, :show?
end
def render_response(response)
diff --git a/app/controllers/concerns/access_token_auth_helper.rb b/app/controllers/concerns/access_token_auth_helper.rb
index 9b0f9021f..338b290da 100644
--- a/app/controllers/concerns/access_token_auth_helper.rb
+++ b/app/controllers/concerns/access_token_auth_helper.rb
@@ -14,6 +14,7 @@ module AccessTokenAuthHelper
ensure_access_token
render_unauthorized('Invalid Access Token') && return if @access_token.blank?
+ # NOTE: This ensures that current_user is set and available for the rest of the controller actions
@resource = @access_token.owner
Current.user = @resource if allowed_current_user_type?(@resource)
end
diff --git a/app/javascript/dashboard/api/changelog.js b/app/javascript/dashboard/api/changelog.js
new file mode 100644
index 000000000..8cf0cdea1
--- /dev/null
+++ b/app/javascript/dashboard/api/changelog.js
@@ -0,0 +1,16 @@
+import axios from 'axios';
+import ApiClient from './ApiClient';
+import { CHANGELOG_API_URL } from 'shared/constants/links';
+
+class ChangelogApi extends ApiClient {
+ constructor() {
+ super('changelog', { apiVersion: 'v1' });
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ fetchFromHub() {
+ return axios.get(CHANGELOG_API_URL);
+ }
+}
+
+export default new ChangelogApi();
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/AssistantPageEmptyState.vue b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/AssistantPageEmptyState.vue
index 365b78222..a4087d79a 100644
--- a/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/AssistantPageEmptyState.vue
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/AssistantPageEmptyState.vue
@@ -1,4 +1,5 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/changelog-card/GroupedStackedChangelogCard.vue b/app/javascript/dashboard/components-next/changelog-card/GroupedStackedChangelogCard.vue
new file mode 100644
index 000000000..617e5d0f8
--- /dev/null
+++ b/app/javascript/dashboard/components-next/changelog-card/GroupedStackedChangelogCard.vue
@@ -0,0 +1,74 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/changelog-card/StackedChangelogCard.story.vue b/app/javascript/dashboard/components-next/changelog-card/StackedChangelogCard.story.vue
new file mode 100644
index 000000000..26978b422
--- /dev/null
+++ b/app/javascript/dashboard/components-next/changelog-card/StackedChangelogCard.story.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/changelog-card/StackedChangelogCard.vue b/app/javascript/dashboard/components-next/changelog-card/StackedChangelogCard.vue
new file mode 100644
index 000000000..8b2d430a4
--- /dev/null
+++ b/app/javascript/dashboard/components-next/changelog-card/StackedChangelogCard.vue
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+ {{ card.meta_title }}
+
+
+ {{ card.meta_description }}
+
+
+
+
+
![]()
+
+
+
![]()
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlight.vue b/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlight.vue
index d8e145817..03ca38d7e 100644
--- a/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlight.vue
+++ b/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlight.vue
@@ -10,6 +10,7 @@ defineProps({
fallbackThumbnail: { type: String, default: '' },
fallbackThumbnailDark: { type: String, default: '' },
learnMoreUrl: { type: String, default: '' },
+ hideActions: { type: Boolean, default: false },
});
const imageError = ref(false);
@@ -65,7 +66,7 @@ const openLink = link => {
{{ note }}
-
+