diff --git a/AGENTS.md b/AGENTS.md index 2430fae2b..82acfcc25 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -85,8 +85,8 @@ ## Project-Specific - **Translations**: - - Only update `en.yml` and `en.json` - - Other languages are handled by the community + - For product and source-string changes, only update `en.yml` and `en.json`; other languages are handled through Crowdin and the community + - Crowdin-generated translation sync PRs may update non-English locale files; do not flag those changes solely for modifying translated locale files - Backend i18n → `en.yml`, Frontend i18n → `en.json` - **Frontend**: - Use `components-next/` for message bubbles (the rest is being deprecated) diff --git a/Gemfile.lock b/Gemfile.lock index 68674155e..46a1c0449 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -211,13 +211,14 @@ GEM activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - 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) + datadog (2.38.0) + cgi + datadog-ruby_core_source (~> 3.5, >= 3.5.3) + libdatadog (~> 36.0.0.1.0) + libddwaf (~> 1.30.0.0.0) logger msgpack - datadog-ruby_core_source (3.4.1) + datadog-ruby_core_source (3.5.3) date (3.5.1) debug (1.8.0) irb (>= 1.5.0) @@ -522,15 +523,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) + libdatadog (36.0.0.1.0) + libdatadog (36.0.0.1.0-arm64-darwin) + libdatadog (36.0.0.1.0-x86_64-linux) + libddwaf (1.30.0.0.2) ffi (~> 1.0) - libddwaf (1.24.1.0.3-arm64-darwin) + libddwaf (1.30.0.0.2-arm64-darwin) ffi (~> 1.0) - libddwaf (1.24.1.0.3-x86_64-darwin) + libddwaf (1.30.0.0.2-x86_64-darwin) ffi (~> 1.0) - libddwaf (1.24.1.0.3-x86_64-linux) + libddwaf (1.30.0.0.2-x86_64-linux) ffi (~> 1.0) line-bot-api (1.28.0) lint_roller (1.1.0) @@ -547,7 +549,7 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.23.1) + loofah (2.25.2) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -740,8 +742,8 @@ GEM activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.1) - loofah (~> 2.21) + rails-html-sanitizer (1.7.1) + loofah (~> 2.25, >= 2.25.2) 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) rails-i18n (7.0.10) i18n (>= 0.7, < 2) diff --git a/LICENSE b/LICENSE index 8f80f35be..d0425a707 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2017-2024 Chatwoot Inc. +Copyright (c) 2017-2026 Chatwoot Inc. Portions of this software are licensed as follows: diff --git a/VERSION_CW b/VERSION_CW index fb0557132..ecbc3b030 100644 --- a/VERSION_CW +++ b/VERSION_CW @@ -1 +1 @@ -4.15.1 +4.16.0 diff --git a/app/controllers/api/v1/accounts/articles_controller.rb b/app/controllers/api/v1/accounts/articles_controller.rb index 4a8363fdd..439ba3e31 100644 --- a/app/controllers/api/v1/accounts/articles_controller.rb +++ b/app/controllers/api/v1/accounts/articles_controller.rb @@ -30,8 +30,8 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController end def update - @article.update!(article_params) if params[:article].present? - render json: { error: @article.errors.messages }, status: :unprocessable_entity and return unless @article.valid? + persist_article_changes if params[:article].present? + render json: { message: @article.errors.full_messages.to_sentence }, status: :unprocessable_entity and return unless @article.valid? end def destroy @@ -67,12 +67,26 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController @portal ||= Current.account.portals.find_by!(slug: params[:portal_id]) end + # Draft-only autosaves must not bump the public-facing updated_at, so write + # them with update_columns (which skips the timestamp). update_columns also + # skips validations, so assign and validate first to avoid persisting content + # that exceeds the column length limit. + def persist_article_changes + keys = article_params.to_h.keys + if keys.any? && (keys - %w[draft_title draft_content]).empty? + @article.assign_attributes(article_params) + @article.update_columns(article_params.to_h) if @article.valid? # rubocop:disable Rails/SkipsModelValidations + else + @article.update!(article_params) + end + end + def article_params params.require(:article).permit( :title, :slug, :position, :content, :description, :category_id, :author_id, :associated_article_id, :status, - :locale, meta: [:title, - :description, - { tags: [] }] + :locale, :draft_title, :draft_content, meta: [:title, + :description, + { tags: [] }] ) end diff --git a/app/controllers/api/v1/accounts/base_controller.rb b/app/controllers/api/v1/accounts/base_controller.rb index e30effc59..f08b87e60 100644 --- a/app/controllers/api/v1/accounts/base_controller.rb +++ b/app/controllers/api/v1/accounts/base_controller.rb @@ -2,5 +2,14 @@ class Api::V1::Accounts::BaseController < Api::BaseController include SwitchLocale include EnsureCurrentAccountHelper before_action :current_account + before_action :validate_token_api_access, if: :authenticate_by_access_token? around_action :switch_locale_using_account_locale + + private + + def validate_token_api_access + return if Current.account.api_and_webhooks_enabled? + + render json: { error: 'API access is not enabled for this account' }, status: :forbidden + end end diff --git a/app/controllers/api/v1/accounts/branded_email_layouts_controller.rb b/app/controllers/api/v1/accounts/branded_email_layouts_controller.rb new file mode 100644 index 000000000..54f976b79 --- /dev/null +++ b/app/controllers/api/v1/accounts/branded_email_layouts_controller.rb @@ -0,0 +1,28 @@ +class Api::V1::Accounts::BrandedEmailLayoutsController < Api::V1::Accounts::BaseController + before_action :check_admin_authorization? + + def show + set_branded_email_layout + end + + def update + unless Current.account.feature_enabled?(:branded_email_templates) + render_could_not_create_error('Branded email templates feature is not enabled') + return + end + + branded_email_layout = params[:branded_email_layout] == 'null' ? nil : params[:branded_email_layout] + EmailTemplate.update_account_branded_layout!(account: Current.account, body: branded_email_layout) if params.key?(:branded_email_layout) + set_branded_email_layout + rescue ActiveRecord::RecordInvalid => e + render_could_not_create_error(e.record.errors.full_messages.join(', ')) + end + + private + + def set_branded_email_layout + @branded_email_layout = EmailTemplate.account_branded_layout_template_for(Current.account)&.body + end +end + +Api::V1::Accounts::BrandedEmailLayoutsController.prepend_mod_with('Api::V1::Accounts::BrandedEmailLayoutsController') diff --git a/app/controllers/api/v1/accounts/conversations/direct_uploads_controller.rb b/app/controllers/api/v1/accounts/conversations/direct_uploads_controller.rb index f4ac05d6e..915ade8a3 100644 --- a/app/controllers/api/v1/accounts/conversations/direct_uploads_controller.rb +++ b/app/controllers/api/v1/accounts/conversations/direct_uploads_controller.rb @@ -1,6 +1,17 @@ class Api::V1::Accounts::Conversations::DirectUploadsController < ActiveStorage::DirectUploadsController + include DeviseTokenAuth::Concerns::SetUserByToken + include RequestExceptionHandler + include AccessTokenAuthHelper include EnsureCurrentAccountHelper + + skip_before_action :verify_authenticity_token, if: :authenticate_by_access_token? + + around_action :handle_with_exception + before_action :authenticate_access_token!, if: :authenticate_by_access_token? + before_action :validate_bot_access_token!, if: :authenticate_by_access_token? + before_action :authenticate_user!, unless: :authenticate_by_access_token? before_action :current_account + before_action :validate_token_api_access, if: :authenticate_by_access_token? before_action :conversation def create @@ -11,6 +22,16 @@ class Api::V1::Accounts::Conversations::DirectUploadsController < ActiveStorage: private + def authenticate_by_access_token? + request.headers[:api_access_token].present? || request.headers[:HTTP_API_ACCESS_TOKEN].present? + end + + def validate_token_api_access + return if Current.account.api_and_webhooks_enabled? + + render json: { error: 'API access is not enabled for this account' }, status: :forbidden + end + def conversation @conversation ||= Current.account.conversations.find_by(display_id: params[:conversation_id]) end diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index bb9ca2a70..822fce7db 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -45,11 +45,20 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController end def update - inbox_params = permitted_params.except(:channel, :csat_config) - inbox_params[:csat_config] = format_csat_config(permitted_params[:csat_config]) if permitted_params[:csat_config].present? - @inbox.update!(inbox_params) - update_inbox_working_hours - update_channel if channel_update_required? + continue_update = false + + ActiveRecord::Base.transaction do + continue_update = update_branded_email_layout + raise ActiveRecord::Rollback unless continue_update + + inbox_params = permitted_params.except(:channel, :csat_config) + inbox_params[:csat_config] = format_csat_config(permitted_params[:csat_config]) if permitted_params[:csat_config].present? + @inbox.update!(inbox_params) + update_inbox_working_hours + update_channel if channel_update_required? + end + + return unless continue_update end def agent_bot @@ -155,6 +164,34 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController formatted['template'] = config['template'] if config['template'].present? end + def update_branded_email_layout + return true unless params.key?(:branded_email_layout) + + branded_email_layout = normalized_branded_email_layout + + unless Current.account.feature_enabled?(:branded_email_templates) + return true if branded_email_layout.blank? + + render_could_not_create_error('Branded email templates feature is not enabled') + return false + end + + unless @inbox.email? + return true if branded_email_layout.blank? + + render_could_not_create_error('Branded email layout is only supported for email inboxes') + return false + end + + @inbox.update_branded_email_layout!(branded_email_layout) + true + rescue ActiveRecord::RecordInvalid => e + render_could_not_create_error(e.record.errors.full_messages.join(', ')) + false + end + + def normalized_branded_email_layout = params[:branded_email_layout] == 'null' ? nil : params[:branded_email_layout] + def inbox_attributes [:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled, :enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved, diff --git a/app/controllers/api/v1/accounts/portals_controller.rb b/app/controllers/api/v1/accounts/portals_controller.rb index c74c0ecfc..bdaf82f6c 100644 --- a/app/controllers/api/v1/accounts/portals_controller.rb +++ b/app/controllers/api/v1/accounts/portals_controller.rb @@ -81,7 +81,8 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController :name, :page_title, :slug, :archived, { config: [:default_locale, :layout, { allowed_locales: [] }, { draft_locales: [] }, { social_profiles: %i[facebook x instagram linkedin youtube tiktok github whatsapp] }, - { locale_translations: locale_translation_keys.index_with { %i[name page_title header_text] } }] } + { locale_translations: locale_translation_keys.index_with { %i[name page_title header_text] } }, + { popular_content: popular_content_keys.index_with { { category_ids: [], article_ids: [] } } }] } ) end @@ -89,6 +90,10 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController params.dig(:portal, :config, :locale_translations)&.keys || [] end + def popular_content_keys + params.dig(:portal, :config, :popular_content)&.keys || [] + end + def live_chat_widget_params permitted_params = params.permit(:inbox_id) return {} unless permitted_params.key?(:inbox_id) diff --git a/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb b/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb index db94113d9..580ae77c6 100644 --- a/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb +++ b/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb @@ -1,4 +1,6 @@ class Api::V1::Accounts::Whatsapp::AuthorizationsController < Api::V1::Accounts::BaseController + # Reconfiguring/reauthorizing a live inbox swaps its credentials, so restrict it to admins. + before_action :check_admin_authorization?, if: -> { params[:inbox_id].present? } before_action :fetch_and_validate_inbox, if: -> { params[:inbox_id].present? } # POST /api/v1/accounts/:account_id/whatsapp/authorization @@ -31,7 +33,7 @@ class Api::V1::Accounts::Whatsapp::AuthorizationsController < Api::V1::Accounts: end def validate_reauthorization_required - return if @inbox.channel.reauthorization_required? || can_upgrade_to_embedded_signup? + return if @inbox.channel.reauthorization_required? || can_reconfigure_channel? render json: { success: false, @@ -39,10 +41,13 @@ class Api::V1::Accounts::Whatsapp::AuthorizationsController < Api::V1::Accounts: }, status: :unprocessable_entity end - def can_upgrade_to_embedded_signup? + def can_reconfigure_channel? channel = @inbox.channel return false unless channel.provider == 'whatsapp_cloud' + # Reconfiguring a live embedded-signup channel requires the feature flag. + return Current.account.feature_enabled?('whatsapp_reconfigure') if channel.provider_config['source'] == 'embedded_signup' + true end diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index fb991949a..9438a1660 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -8,6 +8,7 @@ class Api::V1::AccountsController < Api::BaseController before_action :ensure_account_name, only: [:create] before_action :validate_captcha, only: [:create] before_action :fetch_account, except: [:create] + before_action :validate_token_api_access, if: :authenticate_by_access_token?, except: [:create] before_action :check_authorization, except: [:create] rescue_from CustomExceptions::Account::InvalidEmail, @@ -105,6 +106,12 @@ class Api::V1::AccountsController < Api::BaseController @current_account_user = @account.account_users.find_by(user_id: current_user.id) end + def validate_token_api_access + return if @account.api_and_webhooks_enabled? + + render json: { error: 'API access is not enabled for this account' }, status: :forbidden + end + def account_params params.permit(:account_name, :email, :name, :password, :locale, :domain, :support_email, :user_full_name) end diff --git a/app/controllers/concerns/ensure_current_account_helper.rb b/app/controllers/concerns/ensure_current_account_helper.rb index ea36a48f2..7ed39fa98 100644 --- a/app/controllers/concerns/ensure_current_account_helper.rb +++ b/app/controllers/concerns/ensure_current_account_helper.rb @@ -14,6 +14,8 @@ module EnsureCurrentAccountHelper account_accessible_for_user?(account) elsif @resource.is_a?(AgentBot) account_accessible_for_bot?(account) + else + render_unauthorized(I18n.t('errors.account.not_authorized')) end account end @@ -21,7 +23,7 @@ module EnsureCurrentAccountHelper def account_accessible_for_user?(account) @current_account_user = account.account_users.find_by(user_id: current_user.id) Current.account_user = @current_account_user - render_unauthorized('You are not authorized to access this account') unless @current_account_user + render_unauthorized(I18n.t('errors.account.not_authorized')) unless @current_account_user end def account_accessible_for_bot?(account) diff --git a/app/controllers/concerns/portal_home_data.rb b/app/controllers/concerns/portal_home_data.rb index 633071301..71eaad081 100644 --- a/app/controllers/concerns/portal_home_data.rb +++ b/app/controllers/concerns/portal_home_data.rb @@ -4,17 +4,53 @@ module PortalHomeData private def load_home_data - base_articles = @portal.articles.published.where(locale: @locale).includes(:author, :category) + load_recommended_content + # The classic hero only needs the recommendations above; the rest is + # documentation-layout home data (also used on custom-domain home pages). + return unless @portal.layout == 'documentation' + @visible_categories = @portal.categories .where(locale: @locale) .joins(:articles).where(articles: { status: :published }) .order(position: :asc) .group('categories.id') - @popular_topics = @visible_categories.first(3) + @popular_topics = @recommended_categories.presence || @visible_categories.first(3) @featured = base_articles.order_by_views.limit(6) @category_contributors = build_category_contributors(@visible_categories) end + def load_recommended_content + @recommended_categories = recommended_categories + @recommended_articles = recommended_articles + end + + def base_articles + @base_articles ||= @portal.articles.published.where(locale: @locale).includes(:author, :category) + end + + # Admin-recommended categories for the locale, in the chosen order. Unlike the + # position-based fallback, published articles aren't required: the admin's pick wins. + def recommended_categories + ids = @portal.popular_category_ids(@locale) + ordered_by_ids(@portal.categories.where(locale: @locale, id: ids), ids) + end + + # Admin-recommended articles for the locale, in the chosen order, limited to + # published articles that still exist. + def recommended_articles + ids = @portal.popular_article_ids(@locale) + ordered_by_ids(base_articles.where(id: ids), ids) + end + + # Loads the scope and returns its records ordered to match `ids`, dropping any + # that no longer exist. Skips the query entirely when `ids` is blank. + def ordered_by_ids(scope, ids) + return [] if ids.blank? + + by_id = scope.index_by(&:id) + ids.filter_map { |id| by_id[id] } + end + def build_category_contributors(categories) category_ids = categories.map(&:id) return {} if category_ids.empty? diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index a369830b6..a72687b42 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -64,10 +64,8 @@ class DashboardController < ActionController::Base return unless @portal @locale = @portal.default_locale - if @portal.layout == 'documentation' - request.variant = :documentation - load_home_data - end + request.variant = :documentation if @portal.layout == 'documentation' + load_home_data render 'public/api/v1/portals/show', layout: 'portal', portal: @portal and return end diff --git a/app/controllers/public/api/v1/portals/base_controller.rb b/app/controllers/public/api/v1/portals/base_controller.rb index 323440304..7b592fbd8 100644 --- a/app/controllers/public/api/v1/portals/base_controller.rb +++ b/app/controllers/public/api/v1/portals/base_controller.rb @@ -24,7 +24,11 @@ class Public::Api::V1::Portals::BaseController < PublicController end def set_view_variant - request.variant = :documentation if @portal_layout == 'documentation' && !@is_plain_layout_enabled + request.variant = if @is_plain_layout_enabled + :plain + elsif @portal_layout == 'documentation' + :documentation + end end def portal @@ -65,6 +69,8 @@ class Public::Api::V1::Portals::BaseController < PublicController def render_404 portal + # set_locale can render_404 before the child's set_view_variant runs; set it here so plain 404s stay chrome-less + set_view_variant render 'public/api/v1/portals/error/404', status: :not_found end diff --git a/app/controllers/public/api/v1/portals_controller.rb b/app/controllers/public/api/v1/portals_controller.rb index 4982278d7..da7d9e6a5 100644 --- a/app/controllers/public/api/v1/portals_controller.rb +++ b/app/controllers/public/api/v1/portals_controller.rb @@ -7,7 +7,7 @@ class Public::Api::V1::PortalsController < Public::Api::V1::Portals::BaseControl before_action :set_portal_layout before_action :set_view_variant before_action :ensure_portal_feature_enabled - before_action :load_home_data, only: [:show], if: -> { @portal_layout == 'documentation' } + before_action :load_home_data, only: [:show], unless: -> { @is_plain_layout_enabled } layout 'portal' def show diff --git a/app/drops/conversation_drop.rb b/app/drops/conversation_drop.rb index d62642885..f25435ff0 100644 --- a/app/drops/conversation_drop.rb +++ b/app/drops/conversation_drop.rb @@ -1,6 +1,10 @@ class ConversationDrop < BaseDrop include MessageFormatHelper + def id + @obj.try(:display_id) + end + def display_id @obj.try(:display_id) end diff --git a/app/drops/inbox_drop.rb b/app/drops/inbox_drop.rb index 563916d5f..29698fb4b 100644 --- a/app/drops/inbox_drop.rb +++ b/app/drops/inbox_drop.rb @@ -2,4 +2,18 @@ class InboxDrop < BaseDrop def name @obj.try(:name) end + + def business_name + @obj.try(:sanitized_business_name) + end + + def avatar_url + @obj.try(:avatar_url) + end + + def email + return unless @obj.try(:email?) + + @obj.try(:email_address).presence || @obj.try(:channel).try(:email) + end end diff --git a/app/javascript/dashboard/api/helpCenter/articles.js b/app/javascript/dashboard/api/helpCenter/articles.js index 55b620d1a..610ba1e57 100644 --- a/app/javascript/dashboard/api/helpCenter/articles.js +++ b/app/javascript/dashboard/api/helpCenter/articles.js @@ -17,6 +17,7 @@ class ArticlesAPI extends PortalsAPI { categorySlug, sort, query, + signal, }) { const url = getArticleSearchURL({ pageNumber, @@ -30,7 +31,7 @@ class ArticlesAPI extends PortalsAPI { host: this.url, }); - return axios.get(url); + return axios.get(url, { signal }); } searchArticles({ portalSlug, query }) { diff --git a/app/javascript/dashboard/api/specs/article.spec.js b/app/javascript/dashboard/api/specs/article.spec.js index b40613739..9f7052ed5 100644 --- a/app/javascript/dashboard/api/specs/article.spec.js +++ b/app/javascript/dashboard/api/specs/article.spec.js @@ -37,7 +37,8 @@ describe('#PortalAPI', () => { authorId: '1', }); expect(axiosMock.get).toHaveBeenCalledWith( - '/api/v1/portals/room-rental/articles?page=1&locale=en-US&status=published&author_id=1' + '/api/v1/portals/room-rental/articles?page=1&locale=en-US&status=published&author_id=1', + { signal: undefined } ); }); }); diff --git a/app/javascript/dashboard/components-next/HelpCenter/ArticleCard/ArticleCard.vue b/app/javascript/dashboard/components-next/HelpCenter/ArticleCard/ArticleCard.vue index 564888f0d..4d0cd0c71 100644 --- a/app/javascript/dashboard/components-next/HelpCenter/ArticleCard/ArticleCard.vue +++ b/app/javascript/dashboard/components-next/HelpCenter/ArticleCard/ArticleCard.vue @@ -1,5 +1,5 @@ + + diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditor.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditor.vue index 59c710a37..6540b6460 100644 --- a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditor.vue +++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditor.vue @@ -1,6 +1,6 @@ diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/LocalePage/PopularContentDialog.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/LocalePage/PopularContentDialog.vue new file mode 100644 index 000000000..487b657bb --- /dev/null +++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/LocalePage/PopularContentDialog.vue @@ -0,0 +1,235 @@ + + + diff --git a/app/javascript/dashboard/components-next/button/ConfirmButton.vue b/app/javascript/dashboard/components-next/button/ConfirmButton.vue index 854d5d452..00b9300c6 100644 --- a/app/javascript/dashboard/components-next/button/ConfirmButton.vue +++ b/app/javascript/dashboard/components-next/button/ConfirmButton.vue @@ -14,6 +14,7 @@ const props = defineProps({ icon: { type: [String, Object, Function], default: '' }, trailingIcon: { type: Boolean, default: false }, isLoading: { type: Boolean, default: false }, + disabled: { type: Boolean, default: false }, }); const emit = defineEmits(['click']); @@ -61,6 +62,7 @@ const handleClick = () => { :icon="icon" :trailing-icon="trailingIcon" :is-loading="isLoading" + :disabled="disabled" @click="handleClick" @blur="resetConfirmMode" > diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/switcher/AssistantSwitcher.vue b/app/javascript/dashboard/components-next/captain/pageComponents/switcher/AssistantSwitcher.vue index c78842241..01562385f 100644 --- a/app/javascript/dashboard/components-next/captain/pageComponents/switcher/AssistantSwitcher.vue +++ b/app/javascript/dashboard/components-next/captain/pageComponents/switcher/AssistantSwitcher.vue @@ -88,7 +88,7 @@ const openCreateAssistantDialog = () => {