Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7edbe3a2b | ||
|
|
bac921133a |
@@ -11,7 +11,7 @@ saml:
|
|||||||
enterprise: true
|
enterprise: true
|
||||||
custom_branding:
|
custom_branding:
|
||||||
name: 'Custom Branding'
|
name: 'Custom Branding'
|
||||||
description: 'Apply your own branding to this installation.'
|
description: 'Apply your own branding to this installation, including email template layout customization.'
|
||||||
enabled: <%= (ChatwootHub.pricing_plan != 'community') %>
|
enabled: <%= (ChatwootHub.pricing_plan != 'community') %>
|
||||||
icon: 'icon-paint-brush-line'
|
icon: 'icon-paint-brush-line'
|
||||||
config_key: 'custom_branding'
|
config_key: 'custom_branding'
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
module SuperAdmin::NavigationHelper
|
module SuperAdmin::NavigationHelper
|
||||||
def settings_open?
|
def settings_open?
|
||||||
params[:controller].in? %w[super_admin/settings super_admin/app_configs]
|
return false if params[:controller] == 'super_admin/email_layouts' && params[:account_id].present?
|
||||||
|
|
||||||
|
params[:controller].in? %w[super_admin/settings super_admin/app_configs super_admin/email_layouts]
|
||||||
end
|
end
|
||||||
|
|
||||||
def settings_pages
|
def settings_pages
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class EmailTemplate < ApplicationRecord
|
|||||||
enum :template_type, { layout: 0, content: 1 }
|
enum :template_type, { layout: 0, content: 1 }
|
||||||
belongs_to :account, optional: true
|
belongs_to :account, optional: true
|
||||||
|
|
||||||
validates :name, uniqueness: { scope: :account }
|
validates :name, uniqueness: { scope: [:account, :template_type, :locale] }
|
||||||
|
|
||||||
def self.resolver(options = {})
|
def self.resolver(options = {})
|
||||||
::EmailTemplates::DbResolverService.using self, options
|
::EmailTemplates::DbResolverService.using self, options
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ class ::EmailTemplates::DbResolverService < ActionView::Resolver
|
|||||||
def find_templates(name, prefix, partial, *_args)
|
def find_templates(name, prefix, partial, *_args)
|
||||||
@template_name = name
|
@template_name = name
|
||||||
@template_type = prefix.include?('layout') ? 'layout' : 'content'
|
@template_type = prefix.include?('layout') ? 'layout' : 'content'
|
||||||
|
@_prefix = prefix
|
||||||
|
@_details = _args.first || {}
|
||||||
@db_template = find_db_template
|
@db_template = find_db_template
|
||||||
|
|
||||||
return [] if @db_template.blank?
|
return [] if @db_template.blank?
|
||||||
@@ -60,11 +62,23 @@ class ::EmailTemplates::DbResolverService < ActionView::Resolver
|
|||||||
def find_account_template
|
def find_account_template
|
||||||
return unless Current.account
|
return unless Current.account
|
||||||
|
|
||||||
@@model.find_by(name: @template_name, template_type: @template_type, account: Current.account)
|
find_template_for(Current.account)
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_installation_template
|
def find_installation_template
|
||||||
@@model.find_by(name: @template_name, template_type: @template_type, account: nil)
|
find_template_for(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_template_for(account)
|
||||||
|
@@model.find_by(name: db_template_name, template_type: @template_type, locale: locale_from_details, account: account) ||
|
||||||
|
@@model.find_by(name: @template_name, template_type: @template_type, locale: locale_from_details, account: account)
|
||||||
|
end
|
||||||
|
|
||||||
|
def locale_from_details
|
||||||
|
return :en if @_details.blank?
|
||||||
|
|
||||||
|
locale = Array(@_details[:locale]).first
|
||||||
|
EmailTemplate.locales.key?(locale.to_s) ? locale : :en
|
||||||
end
|
end
|
||||||
|
|
||||||
# Build path with eventual prefix
|
# Build path with eventual prefix
|
||||||
@@ -72,6 +86,12 @@ class ::EmailTemplates::DbResolverService < ActionView::Resolver
|
|||||||
prefix.present? ? "#{prefix}/#{@template_name}" : @template_name
|
prefix.present? ? "#{prefix}/#{@template_name}" : @template_name
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def db_template_name
|
||||||
|
return @template_name if @template_type == 'layout'
|
||||||
|
|
||||||
|
build_path(@_prefix)
|
||||||
|
end
|
||||||
|
|
||||||
# returns a path depending if its a partial or template
|
# returns a path depending if its a partial or template
|
||||||
# params path: path/to/file.ext partial: true/false
|
# params path: path/to/file.ext partial: true/false
|
||||||
# the function appends _to make the file name _file.ext if partial: true
|
# the function appends _to make the file name _file.ext if partial: true
|
||||||
|
|||||||
@@ -14,66 +14,72 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<section class="main-content__body">
|
<section class="main-content__body relative">
|
||||||
|
<%= render partial: 'email_layout' if @config == 'custom_branding' && ChatwootApp.enterprise? %>
|
||||||
|
|
||||||
<%= form_with url: super_admin_app_config_url(config: @config) , method: :post do |form| %>
|
<%= form_with url: super_admin_app_config_url(config: @config) , method: :post do |form| %>
|
||||||
<% @allowed_configs.each do |key| %>
|
<% @allowed_configs.each do |key| %>
|
||||||
<div class="flex mb-8">
|
<div class="flex mb-8">
|
||||||
<div class="field-unit__label">
|
<div class="field-unit__label">
|
||||||
<%= form.label "app_config[#{key}]", @installation_configs[key]&.dig('display_title') || key %>
|
<%= form.label "app_config[#{key}]", @installation_configs[key]&.dig('display_title') || key %>
|
||||||
</div>
|
</div>
|
||||||
<div class="-mt-2 field-unit__field ">
|
<div class="-mt-2 field-unit__field ">
|
||||||
<% if @installation_configs[key]&.dig('type') == 'boolean' %>
|
<% if @installation_configs[key]&.dig('type') == 'boolean' %>
|
||||||
<%= form.select "app_config[#{key}]",
|
<%= form.select "app_config[#{key}]",
|
||||||
[["True", true], ["False", false]],
|
[["True", true], ["False", false]],
|
||||||
{ selected: ActiveModel::Type::Boolean.new.cast(@app_config[key]) },
|
{ selected: ActiveModel::Type::Boolean.new.cast(@app_config[key]) },
|
||||||
class: "mt-2 border border-slate-100 p-1 rounded-md"
|
class: "mt-2 border border-slate-100 p-1 rounded-md"
|
||||||
%>
|
|
||||||
<% elsif @installation_configs[key]&.dig('type') == 'code' %>
|
|
||||||
<%= form.text_area "app_config[#{key}]",
|
|
||||||
value: @app_config[key],
|
|
||||||
rows: 12,
|
|
||||||
wrap: 'off',
|
|
||||||
class: "mt-2 border font-mono text-xs border-slate-100 p-1 rounded-md overflow-scroll"
|
|
||||||
%>
|
|
||||||
<% elsif @installation_configs[key]&.dig('type') == 'secret' %>
|
|
||||||
<div class="relative">
|
|
||||||
<%= form.password_field "app_config[#{key}]",
|
|
||||||
id: "app_config_#{key}",
|
|
||||||
value: @app_config[key],
|
|
||||||
class: "mt-2 border border-slate-100 p-1.5 pr-8 rounded-md w-full"
|
|
||||||
%>
|
%>
|
||||||
<button
|
<% elsif @installation_configs[key]&.dig('type') == 'code' %>
|
||||||
type="button"
|
<%= form.text_area "app_config[#{key}]",
|
||||||
class="absolute reset-base !bg-white top-1/2 !outline-0 !text-n-slate-11 -translate-y-1/2 right-2 p-1 hover:!bg-n-slate-5 rounded-sm toggle-password"
|
value: @app_config[key],
|
||||||
data-target="app_config_<%= key %>"
|
rows: 12,
|
||||||
>
|
wrap: 'off',
|
||||||
<svg class="eye-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
class: "mt-2 border font-mono text-xs border-slate-100 p-1 rounded-md overflow-scroll"
|
||||||
<path d="M12 5C5.63636 5 2 12 2 12C2 12 5.63636 19 12 19C18.3636 19 22 12 22 12C22 12 18.3636 5 12 5Z"/>
|
%>
|
||||||
<path d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z"/>
|
<% elsif @installation_configs[key]&.dig('type') == 'secret' %>
|
||||||
</svg>
|
<div class="relative">
|
||||||
</button>
|
<%= form.password_field "app_config[#{key}]",
|
||||||
</div>
|
id: "app_config_#{key}",
|
||||||
<% elsif @installation_configs[key]&.dig('type') == 'select' && @installation_configs[key]&.dig('options').present? %>
|
value: @app_config[key],
|
||||||
<%= form.select "app_config[#{key}]",
|
class: "mt-2 border border-slate-100 p-1.5 pr-8 rounded-md w-full"
|
||||||
@installation_configs[key]['options'].map { |val, label| [label, val] },
|
%>
|
||||||
{ selected: @app_config[key] },
|
<button
|
||||||
class: "mt-2 border border-slate-100 p-1 rounded-md"
|
type="button"
|
||||||
%>
|
class="absolute reset-base !bg-white top-1/2 !outline-0 !text-n-slate-11 -translate-y-1/2 right-2 p-1 hover:!bg-n-slate-5 rounded-sm toggle-password"
|
||||||
<% else %>
|
data-target="app_config_<%= key %>"
|
||||||
<%= form.text_field "app_config[#{key}]", value: @app_config[key] %>
|
>
|
||||||
<% end %>
|
<svg class="eye-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<%if @installation_configs[key]&.dig('description').present? %>
|
<path d="M12 5C5.63636 5 2 12 2 12C2 12 5.63636 19 12 19C18.3636 19 22 12 22 12C22 12 18.3636 5 12 5Z"/>
|
||||||
<p class="pt-2 text-xs italic text-slate-400">
|
<path d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z"/>
|
||||||
<%= @installation_configs[key]&.dig('description') %>
|
</svg>
|
||||||
</p>
|
</button>
|
||||||
<% end %>
|
</div>
|
||||||
|
<% elsif @installation_configs[key]&.dig('type') == 'select' && @installation_configs[key]&.dig('options').present? %>
|
||||||
|
<%= form.select "app_config[#{key}]",
|
||||||
|
@installation_configs[key]['options'].map { |val, label| [label, val] },
|
||||||
|
{ selected: @app_config[key] },
|
||||||
|
class: "mt-2 border border-slate-100 p-1 rounded-md"
|
||||||
|
%>
|
||||||
|
<% else %>
|
||||||
|
<%= form.text_field "app_config[#{key}]", value: @app_config[key] %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if @installation_configs[key]&.dig('description').present? %>
|
||||||
|
<p class="pt-2 text-xs italic text-slate-400">
|
||||||
|
<%= @installation_configs[key]&.dig('description') %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<%= form.submit "Submit" %>
|
<%= form.submit "Submit" %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<% content_for :javascript do %>
|
<% content_for :javascript do %>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<li class="px-4 mb-1">
|
<li class="px-4 mb-1">
|
||||||
<% text_class_name = current_page?(url) ? 'text-woot-500 bg-slate-25' : 'text-slate-800' %>
|
<% text_class_name = local_assigns.fetch(:active, current_page?(url)) ? 'text-woot-500 bg-slate-25' : 'text-slate-800' %>
|
||||||
<%= link_to(url, class: text_class_name + " -ml-1 focus:outline-none cursor-pointer flex items-center px-2 py-1.5 text-slate-800 cursor-pointer hover:text-woot-500 hover:bg-slate-25 rounded-lg") do %>
|
<%= link_to(url, class: text_class_name + " -ml-1 focus:outline-none cursor-pointer flex items-center px-2 py-1.5 text-slate-800 cursor-pointer hover:text-woot-500 hover:bg-slate-25 rounded-lg") do %>
|
||||||
<svg width="16" height="16"><use xlink:href="#<%= icon %>" /></svg>
|
<svg width="16" height="16"><use xlink:href="#<%= icon %>" /></svg>
|
||||||
<span class="ml-2 text-sm"><%= label %></span>
|
<span class="ml-2 text-sm"><%= label %></span>
|
||||||
|
|||||||
@@ -33,12 +33,15 @@ as defined by the routes in the `admin/` namespace
|
|||||||
<ul class="my-4">
|
<ul class="my-4">
|
||||||
<%= render partial: "nav_item", locals: { icon: 'icon-grid-line', url: super_admin_root_url, label: 'Dashboard' } %>
|
<%= render partial: "nav_item", locals: { icon: 'icon-grid-line', url: super_admin_root_url, label: 'Dashboard' } %>
|
||||||
<% Administrate::Namespace.new(namespace).resources.each do |resource| %>
|
<% Administrate::Namespace.new(namespace).resources.each do |resource| %>
|
||||||
<% next if ["account_users", "access_tokens", "installation_configs", "dashboard", "devise/sessions", "app_configs", "instance_statuses", "settings", "push_diagnostics"].include? resource.resource %>
|
<% next if ["account_users", "access_tokens", "installation_configs", "dashboard", "devise/sessions", "app_configs", "instance_statuses", "settings", "push_diagnostics", "email_layouts"].include? resource.resource %>
|
||||||
<% next if resource.resource == "platform_banners" && !ChatwootApp.chatwoot_cloud? %>
|
<% next if resource.resource == "platform_banners" && !ChatwootApp.chatwoot_cloud? %>
|
||||||
|
<% resource_url = resource_index_route(resource) %>
|
||||||
|
<% active = current_page?(resource_url) || (resource.resource == 'accounts' && params[:controller] == 'super_admin/email_layouts' && params[:account_id].present?) %>
|
||||||
<%= render partial: "nav_item", locals: {
|
<%= render partial: "nav_item", locals: {
|
||||||
icon: sidebar_icons[resource.resource.to_sym],
|
icon: sidebar_icons[resource.resource.to_sym],
|
||||||
url: resource_index_route(resource),
|
url: resource_url,
|
||||||
label: display_resource_name(resource),
|
label: display_resource_name(resource),
|
||||||
|
active: active,
|
||||||
}
|
}
|
||||||
%>
|
%>
|
||||||
|
|
||||||
|
|||||||
@@ -685,6 +685,7 @@ Rails.application.routes.draw do
|
|||||||
resources :accounts, only: [:index, :new, :create, :show, :edit, :update, :destroy] do
|
resources :accounts, only: [:index, :new, :create, :show, :edit, :update, :destroy] do
|
||||||
post :seed, on: :member
|
post :seed, on: :member
|
||||||
post :reset_cache, on: :member
|
post :reset_cache, on: :member
|
||||||
|
resource :email_layout, only: [:edit, :update, :destroy] if ChatwootApp.enterprise?
|
||||||
end
|
end
|
||||||
resources :users, only: [:index, :new, :create, :show, :edit, :update, :destroy] do
|
resources :users, only: [:index, :new, :create, :show, :edit, :update, :destroy] do
|
||||||
delete :avatar, on: :member, action: :destroy_avatar
|
delete :avatar, on: :member, action: :destroy_avatar
|
||||||
@@ -702,6 +703,7 @@ Rails.application.routes.draw do
|
|||||||
resource :settings, only: [:show] do
|
resource :settings, only: [:show] do
|
||||||
get :refresh, on: :collection
|
get :refresh, on: :collection
|
||||||
end
|
end
|
||||||
|
resource :email_layout, only: [:edit, :update, :destroy] if ChatwootApp.enterprise?
|
||||||
|
|
||||||
# resources that doesn't appear in primary navigation in super admin
|
# resources that doesn't appear in primary navigation in super admin
|
||||||
resources :account_users, only: [:new, :create, :show, :destroy]
|
resources :account_users, only: [:new, :create, :show, :destroy]
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
class UpdateEmailTemplatesUniqueIndexForLocaleAndType < ActiveRecord::Migration[7.1]
|
||||||
|
def change
|
||||||
|
remove_index :email_templates, name: 'index_email_templates_on_name_and_account_id'
|
||||||
|
add_index :email_templates,
|
||||||
|
[:name, :account_id, :template_type, :locale],
|
||||||
|
unique: true,
|
||||||
|
name: 'index_email_templates_on_name_account_type_locale'
|
||||||
|
end
|
||||||
|
end
|
||||||
+1
-1
@@ -859,7 +859,7 @@ ActiveRecord::Schema[7.1].define(version: 2026_06_20_000000) do
|
|||||||
t.integer "locale", default: 0, null: false
|
t.integer "locale", default: 0, null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.index ["name", "account_id"], name: "index_email_templates_on_name_and_account_id", unique: true
|
t.index ["name", "account_id", "template_type", "locale"], name: "index_email_templates_on_name_account_type_locale", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "folders", force: :cascade do |t|
|
create_table "folders", force: :cascade do |t|
|
||||||
|
|||||||
@@ -0,0 +1,280 @@
|
|||||||
|
class SuperAdmin::EmailLayoutsController < SuperAdmin::EnterpriseBaseController
|
||||||
|
before_action :set_account
|
||||||
|
before_action :set_custom_templates
|
||||||
|
before_action :set_template_selection
|
||||||
|
before_action :set_email_layout
|
||||||
|
before_action :ensure_custom_branding_access!
|
||||||
|
|
||||||
|
def edit
|
||||||
|
@show_template_editor = editor_requested?
|
||||||
|
return unless @show_template_editor
|
||||||
|
|
||||||
|
@layout_body = @email_layout&.body || inherited_template_body
|
||||||
|
set_preview_context
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@layout_body = layout_params[:body].to_s
|
||||||
|
validation_error = validate_layout_body(@layout_body)
|
||||||
|
|
||||||
|
return render_edit_with_alert(validation_error) if validation_error.present?
|
||||||
|
|
||||||
|
return render_edit_with_alert(conflict_error) if conflicting_template_exists?
|
||||||
|
|
||||||
|
@email_layout ||= EmailTemplate.new(name: template_name, template_type: template_type, locale: template_locale, account: @account)
|
||||||
|
@email_layout.body = @layout_body
|
||||||
|
|
||||||
|
if @email_layout.save
|
||||||
|
# rubocop:disable Rails/I18nLocaleTexts
|
||||||
|
redirect_to edit_email_layout_path, notice: 'Email layout updated successfully'
|
||||||
|
# rubocop:enable Rails/I18nLocaleTexts
|
||||||
|
else
|
||||||
|
render_edit_with_alert(@email_layout.errors.full_messages.join(', '))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@email_layout&.destroy
|
||||||
|
redirect_to email_layout_overview_path, notice: reset_notice
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_account
|
||||||
|
@account = Account.find(params[:account_id]) if params[:account_id].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_custom_templates
|
||||||
|
@custom_templates = EmailTemplate.where(account: @account).order(:template_type, :name, :locale)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_template_selection
|
||||||
|
@template_type = selected_template_type
|
||||||
|
@template_name = selected_template_name
|
||||||
|
@template_locale = selected_template_locale
|
||||||
|
@template_options = template_options
|
||||||
|
@locale_options = locale_options
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_email_layout
|
||||||
|
@email_layout = EmailTemplate.find_by(name: template_name, template_type: template_type, locale: template_locale, account: @account)
|
||||||
|
end
|
||||||
|
|
||||||
|
attr_reader :template_name, :template_type, :template_locale
|
||||||
|
|
||||||
|
def default_layout_body
|
||||||
|
Rails.root.join('app/views/layouts/mailer/base.liquid').read
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_content_body
|
||||||
|
content_template_path.read
|
||||||
|
end
|
||||||
|
|
||||||
|
def inherited_template_body
|
||||||
|
return global_template_body || default_template_body if @account.present?
|
||||||
|
|
||||||
|
default_template_body
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_template_body
|
||||||
|
return default_layout_body if template_type == 'layout'
|
||||||
|
|
||||||
|
default_content_body
|
||||||
|
end
|
||||||
|
|
||||||
|
def global_template_body
|
||||||
|
EmailTemplate.find_by(name: template_name, template_type: template_type, locale: template_locale, account: nil)&.body
|
||||||
|
end
|
||||||
|
|
||||||
|
def layout_params
|
||||||
|
params.require(:email_layout).permit(:body, :template_type, :template_name, :locale)
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_layout_body(body)
|
||||||
|
return 'Template body cannot be blank' if body.blank?
|
||||||
|
return 'Layout must include {{ content_for_layout }}' if template_type == 'layout' && !body.match?(/\{\{\s*content_for_layout\s*\}\}/)
|
||||||
|
|
||||||
|
Liquid::Template.parse(body)
|
||||||
|
nil
|
||||||
|
rescue Liquid::SyntaxError => e
|
||||||
|
"Liquid syntax error: #{e.message}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_edit_with_alert(message)
|
||||||
|
flash.now[:alert] = message
|
||||||
|
@show_template_editor = true
|
||||||
|
set_preview_context
|
||||||
|
render :edit, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_preview_context
|
||||||
|
@preview_html = render_preview(@layout_body)
|
||||||
|
@supported_variables = supported_variables
|
||||||
|
@template_status = template_status
|
||||||
|
@preview_config = preview_config
|
||||||
|
rescue Liquid::SyntaxError
|
||||||
|
@preview_html = ''
|
||||||
|
@supported_variables = supported_variables
|
||||||
|
@template_status = template_status
|
||||||
|
@preview_config = preview_config
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_preview(body)
|
||||||
|
assigns = {
|
||||||
|
'content_for_layout' => preview_content,
|
||||||
|
'global_config' => GlobalConfig.get('BRAND_NAME', 'BRAND_URL')
|
||||||
|
}
|
||||||
|
assigns['account'] = { 'name' => @account.name } if @account.present?
|
||||||
|
|
||||||
|
rendered_body = Liquid::Template.parse(body).render(assigns)
|
||||||
|
return rendered_body if template_type == 'layout'
|
||||||
|
|
||||||
|
Liquid::Template.parse(default_layout_body).render(assigns.merge('content_for_layout' => rendered_body))
|
||||||
|
end
|
||||||
|
|
||||||
|
def preview_content
|
||||||
|
<<~HTML
|
||||||
|
<tr>
|
||||||
|
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; padding: 0;">
|
||||||
|
<h1 style="font-size: 20px; margin: 0 0 12px;">Sample notification</h1>
|
||||||
|
<p style="margin: 0 0 16px;">This preview shows how notification content renders inside your email layout.</p>
|
||||||
|
<a href="#" style="display: inline-block; background: #1f93ff; color: #fff; padding: 10px 14px; border-radius: 6px; text-decoration: none;">View conversation</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
HTML
|
||||||
|
end
|
||||||
|
|
||||||
|
def preview_config
|
||||||
|
brand_config = GlobalConfig.get('BRAND_NAME', 'BRAND_URL')
|
||||||
|
{
|
||||||
|
templateType: template_type,
|
||||||
|
defaultLayout: default_layout_body,
|
||||||
|
previewContent: preview_content,
|
||||||
|
brandName: brand_config['BRAND_NAME'].to_s,
|
||||||
|
brandUrl: brand_config['BRAND_URL'].to_s,
|
||||||
|
accountName: @account&.name.to_s
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def supported_variables
|
||||||
|
variables = []
|
||||||
|
if template_type == 'layout'
|
||||||
|
variables << {
|
||||||
|
variable: '{{ content_for_layout }}',
|
||||||
|
description: 'Required. Renders the email body inside the layout.'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
variables + [
|
||||||
|
{
|
||||||
|
variable: "{{ global_config['BRAND_NAME'] }}",
|
||||||
|
description: 'Current brand name from Custom Branding.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variable: "{{ global_config['BRAND_URL'] }}",
|
||||||
|
description: 'Current brand URL from Custom Branding.'
|
||||||
|
}
|
||||||
|
].tap do |template_variables|
|
||||||
|
next if @account.blank?
|
||||||
|
|
||||||
|
template_variables << {
|
||||||
|
variable: '{{ account.name }}',
|
||||||
|
description: 'Current account name for account-specific previews.'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def conflict_error
|
||||||
|
"Cannot save template. '#{template_name}' is already used by another email template for this account, type, and language."
|
||||||
|
end
|
||||||
|
|
||||||
|
def conflicting_template_exists?
|
||||||
|
return false if @email_layout.present?
|
||||||
|
|
||||||
|
EmailTemplate.exists?(name: template_name, template_type: template_type, locale: template_locale, account: @account)
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit_email_layout_path
|
||||||
|
query = { template_type: template_type, template_name: template_name, locale: template_locale }
|
||||||
|
return edit_super_admin_account_email_layout_path(@account, query) if @account.present?
|
||||||
|
|
||||||
|
edit_super_admin_email_layout_path(query)
|
||||||
|
end
|
||||||
|
|
||||||
|
def email_layout_overview_path
|
||||||
|
return edit_super_admin_account_email_layout_path(@account) if @account.present?
|
||||||
|
|
||||||
|
edit_super_admin_email_layout_path
|
||||||
|
end
|
||||||
|
|
||||||
|
def editor_requested?
|
||||||
|
params[:new].present? || params[:template_type].present? || params[:template_name].present? || params[:locale].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def reset_notice
|
||||||
|
return 'Account email template reset to inherited template' if @account.present?
|
||||||
|
|
||||||
|
'Email template reset to default'
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_custom_branding_access!
|
||||||
|
return if ChatwootHub.pricing_plan != 'community'
|
||||||
|
|
||||||
|
invalid_action_perfomed
|
||||||
|
end
|
||||||
|
|
||||||
|
def selected_template_type
|
||||||
|
selected_type = params.dig(:email_layout, :template_type).presence || params[:template_type].presence || 'layout'
|
||||||
|
EmailTemplate.template_types.key?(selected_type) ? selected_type : 'layout'
|
||||||
|
end
|
||||||
|
|
||||||
|
def selected_template_name
|
||||||
|
selected_name = params.dig(:email_layout, :template_name).presence || params[:template_name].presence
|
||||||
|
selected_name = default_template_name if selected_name.blank?
|
||||||
|
available_template_names.include?(selected_name) ? selected_name : default_template_name
|
||||||
|
end
|
||||||
|
|
||||||
|
def selected_template_locale
|
||||||
|
selected_locale = params.dig(:email_layout, :locale).presence || params[:locale].presence || 'en'
|
||||||
|
EmailTemplate.locales.key?(selected_locale) ? selected_locale : 'en'
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_template_name
|
||||||
|
return 'base' if template_type == 'layout'
|
||||||
|
|
||||||
|
content_template_options.first[:name]
|
||||||
|
end
|
||||||
|
|
||||||
|
def template_options
|
||||||
|
return [{ name: 'base', label: 'Base layout' }] if template_type == 'layout'
|
||||||
|
|
||||||
|
content_template_options
|
||||||
|
end
|
||||||
|
|
||||||
|
def available_template_names
|
||||||
|
template_options.pluck(:name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def content_template_options
|
||||||
|
@content_template_options ||= Rails.root.glob('app/views/mailers/**/*.liquid').map do |path|
|
||||||
|
name = path.relative_path_from(Rails.root.join('app/views/mailers')).to_s.delete_suffix('.liquid')
|
||||||
|
{ name: name, label: name.humanize }
|
||||||
|
end.sort_by { |template| template[:label] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def content_template_path
|
||||||
|
Rails.root.join('app/views/mailers', "#{template_name}.liquid")
|
||||||
|
end
|
||||||
|
|
||||||
|
def locale_options
|
||||||
|
EmailTemplate.locales.keys.map { |locale| [locale.upcase, locale] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def template_status
|
||||||
|
return 'Account override configured' if @account.present? && @email_layout.present?
|
||||||
|
return 'Global override configured' if @account.blank? && @email_layout.present?
|
||||||
|
return 'Inherits global template' if @account.present? && global_template_body.present?
|
||||||
|
|
||||||
|
'Inherits default template'
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -30,5 +30,22 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<% if ChatwootHub.pricing_plan != 'community' %>
|
||||||
|
<div class="flex items-center justify-between p-3 bg-white rounded-md outline outline-n-container outline-1 shadow-sm">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="bg-n-amber-3 text-n-amber-12 rounded-full p-1 inline-flex right-4 top-5">
|
||||||
|
<svg width="12" height="12"><use xlink:href="#icon-lock-line" /></svg>
|
||||||
|
</span>
|
||||||
|
<span class="text-sm text-n-slate-12">Account Email Templates</span>
|
||||||
|
</div>
|
||||||
|
<%= link_to edit_super_admin_account_email_layout_path(field.resource),
|
||||||
|
aria: { label: 'Customize account email templates' },
|
||||||
|
title: 'Customize account email templates',
|
||||||
|
class: 'inline-flex rounded-full bg-slate-50 p-1 text-slate-800 hover:bg-n-slate-4 hover:text-n-slate-12 focus:outline-none' do %>
|
||||||
|
<svg width="12" height="12"><use xlink:href="#icon-settings-2-line" /></svg>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<% if ChatwootHub.pricing_plan != 'community' %>
|
||||||
|
<div class="absolute right-4 top-5 w-96 rounded-md border border-slate-100 p-4">
|
||||||
|
<div class="flex items-start justify-between gap-5">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<span class="text-sm font-medium text-n-slate-12">Email templates</span>
|
||||||
|
<p class="m-0 text-xs italic leading-5 text-slate-400">
|
||||||
|
Customize the notification email layout for this brand.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<%= link_to 'Manage',
|
||||||
|
edit_super_admin_email_layout_path,
|
||||||
|
class: 'flex h-8 shrink-0 items-center rounded-md bg-transparent px-3 text-sm font-medium text-n-slate-11 outline outline-1 outline-n-container hover:bg-slate-50 hover:text-n-slate-12 focus:outline-none' %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
<% content_for(:title) do %>
|
||||||
|
Configure Settings - <%= @account.present? ? "#{@account.name} Email Templates" : 'Email Templates' %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% overview_path = @account.present? ? edit_super_admin_account_email_layout_path(@account) : edit_super_admin_email_layout_path %>
|
||||||
|
<% new_template_path = @account.present? ? edit_super_admin_account_email_layout_path(@account, new: 1, template_type: 'layout', template_name: 'base', locale: 'en') : edit_super_admin_email_layout_path(new: 1, template_type: 'layout', template_name: 'base', locale: 'en') %>
|
||||||
|
|
||||||
|
<header class="main-content__header" role="banner">
|
||||||
|
<div class="flex items-center justify-between w-full">
|
||||||
|
<h1 class="main-content__page-title m-0 mr-6" id="page-title">
|
||||||
|
<%= content_for(:title) %>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<% unless @show_template_editor %>
|
||||||
|
<div class="whitespace-nowrap ml-4">
|
||||||
|
<%= link_to 'Create new template', new_template_path, class: 'button' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<% unless @show_template_editor %>
|
||||||
|
<section class="main-content__body main-content__body--flush">
|
||||||
|
<table aria-labelledby="page-title">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="cell-label cell-label--string" scope="col">Type</th>
|
||||||
|
<th class="cell-label cell-label--string" scope="col">Template</th>
|
||||||
|
<th class="cell-label cell-label--string" scope="col">Language</th>
|
||||||
|
<th class="cell-label cell-label--string" scope="col">Updated</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<% @custom_templates.each do |template| %>
|
||||||
|
<% template_path = @account.present? ? edit_super_admin_account_email_layout_path(@account, template_type: template.template_type, template_name: template.name, locale: template.locale) : edit_super_admin_email_layout_path(template_type: template.template_type, template_name: template.name, locale: template.locale) %>
|
||||||
|
<tr class="js-table-row" tabindex="0" role="link" data-url="<%= template_path %>">
|
||||||
|
<td class="cell-data cell-data--string">
|
||||||
|
<%= link_to template.template_type.titleize, template_path, class: 'action-show' %>
|
||||||
|
</td>
|
||||||
|
<td class="cell-data cell-data--string">
|
||||||
|
<%= link_to template.name.humanize, template_path, class: 'action-show' %>
|
||||||
|
</td>
|
||||||
|
<td class="cell-data cell-data--string">
|
||||||
|
<%= link_to template.locale.upcase, template_path, class: 'action-show' %>
|
||||||
|
</td>
|
||||||
|
<td class="cell-data cell-data--string">
|
||||||
|
<%= link_to "#{time_ago_in_words(template.updated_at)} ago", template_path, class: 'action-show' %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
<% else %>
|
||||||
|
<section class="main-content__body">
|
||||||
|
<div class="pr-16">
|
||||||
|
<%= form_tag overview_path, method: :get, id: 'email-template-selector-form', autocomplete: 'off' do %>
|
||||||
|
<%= hidden_field_tag :new, 1 %>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Template details</legend>
|
||||||
|
|
||||||
|
<div class="field-unit field-unit--string">
|
||||||
|
<div class="field-unit__label">
|
||||||
|
<%= label_tag :template_type, 'Type' %>
|
||||||
|
</div>
|
||||||
|
<div class="field-unit__field">
|
||||||
|
<%= select_tag :template_type,
|
||||||
|
options_for_select(EmailTemplate.template_types.keys.map { |type| [type.titleize, type] }, @template_type),
|
||||||
|
data: { current_value: @template_type } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-unit field-unit--string">
|
||||||
|
<div class="field-unit__label">
|
||||||
|
<%= label_tag :template_name, 'Template' %>
|
||||||
|
</div>
|
||||||
|
<div class="field-unit__field">
|
||||||
|
<%= select_tag :template_name,
|
||||||
|
options_for_select(@template_options.map { |template| [template[:label], template[:name]] }, @template_name),
|
||||||
|
data: { current_value: @template_name } %>
|
||||||
|
<p class="pt-2 text-xs italic text-slate-400"><%= @template_status %></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-unit field-unit--string">
|
||||||
|
<div class="field-unit__label">
|
||||||
|
<%= label_tag :locale, 'Language' %>
|
||||||
|
</div>
|
||||||
|
<div class="field-unit__field">
|
||||||
|
<%= select_tag :locale,
|
||||||
|
options_for_select(@locale_options, @template_locale),
|
||||||
|
data: { current_value: @template_locale } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<%= submit_tag 'Load' %>
|
||||||
|
<%= link_to 'All templates', overview_path, class: 'button' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= form_with scope: :email_layout,
|
||||||
|
url: @account.present? ? super_admin_account_email_layout_path(@account) : super_admin_email_layout_path,
|
||||||
|
method: :patch do |form| %>
|
||||||
|
<%= form.hidden_field :template_type, value: @template_type %>
|
||||||
|
<%= form.hidden_field :template_name, value: @template_name %>
|
||||||
|
<%= form.hidden_field :locale, value: @template_locale %>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Template body</legend>
|
||||||
|
<div class="field-unit field-unit--text">
|
||||||
|
<div class="field-unit__label">
|
||||||
|
<%= form.label :body, 'HTML / Liquid' %>
|
||||||
|
</div>
|
||||||
|
<div class="field-unit__field">
|
||||||
|
<%= form.text_area :body,
|
||||||
|
value: @layout_body,
|
||||||
|
rows: 24,
|
||||||
|
wrap: 'off',
|
||||||
|
data: { email_template_editor: true },
|
||||||
|
class: 'mt-2 w-full border font-mono text-xs border-slate-100 p-1 rounded-md overflow-scroll' %>
|
||||||
|
<% if @template_type == 'layout' %>
|
||||||
|
<p class="pt-2 text-xs italic text-slate-400">Keep {{ content_for_layout }} in the layout.</p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<%= form.submit @email_layout.present? ? 'Save template' : 'Create template override' %>
|
||||||
|
|
||||||
|
<% if @email_layout.present? %>
|
||||||
|
<%= link_to t('administrate.actions.destroy'),
|
||||||
|
@account.present? ? super_admin_account_email_layout_path(@account, template_type: @template_type, template_name: @template_name, locale: @template_locale) : super_admin_email_layout_path(template_type: @template_type, template_name: @template_name, locale: @template_locale),
|
||||||
|
class: 'button button--danger',
|
||||||
|
method: :delete,
|
||||||
|
data: { confirm: t('administrate.actions.confirm') } %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Preview</legend>
|
||||||
|
<div class="attribute-label">Email layout preview</div>
|
||||||
|
<div class="attribute-data attribute-data--string">
|
||||||
|
<iframe
|
||||||
|
title="Email layout preview"
|
||||||
|
sandbox
|
||||||
|
height="420"
|
||||||
|
srcdoc="<%= @preview_html %>"
|
||||||
|
data-email-template-preview="true"
|
||||||
|
class="w-full rounded-md border border-slate-100 bg-white"
|
||||||
|
></iframe>
|
||||||
|
<p class="pt-2 text-xs italic text-slate-400">Refreshes as you type.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="attribute-label">Variables</div>
|
||||||
|
<div class="attribute-data attribute-data--string">
|
||||||
|
<% @supported_variables.each do |supported_variable| %>
|
||||||
|
<p class="m-0 mb-2 text-xs text-slate-500">
|
||||||
|
<code class="rounded bg-slate-25 px-2 py-1 font-mono text-xs text-slate-700"><%= supported_variable[:variable] %></code>
|
||||||
|
<%= supported_variable[:description] %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<script type="application/json" id="email-preview-config"><%= raw json_escape(@preview_config.to_json) %></script>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if @show_template_editor %>
|
||||||
|
<% content_for :javascript do %>
|
||||||
|
<script>
|
||||||
|
const initializeEmailTemplateEditor = () => {
|
||||||
|
const selectorForm = document.getElementById('email-template-selector-form') || document.querySelector('form[method="get"]');
|
||||||
|
if (selectorForm) {
|
||||||
|
let isSubmitting = false;
|
||||||
|
const submitSelectorForm = select => {
|
||||||
|
if (isSubmitting) return;
|
||||||
|
|
||||||
|
const templateSelect = selectorForm.querySelector('select[name="template_name"]');
|
||||||
|
if (select.name === 'template_type' && templateSelect) {
|
||||||
|
templateSelect.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubmitting = true;
|
||||||
|
selectorForm.requestSubmit();
|
||||||
|
};
|
||||||
|
|
||||||
|
selectorForm.querySelectorAll('select').forEach(select => {
|
||||||
|
if (select.dataset.currentValue) {
|
||||||
|
if (select.selectize) {
|
||||||
|
select.selectize.setValue(select.dataset.currentValue, true);
|
||||||
|
} else {
|
||||||
|
select.value = select.dataset.currentValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select.addEventListener('change', () => submitSelectorForm(select));
|
||||||
|
select.selectize?.on('change', () => submitSelectorForm(select));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const editor = document.querySelector('[data-email-template-editor]');
|
||||||
|
const preview = document.querySelector('[data-email-template-preview]');
|
||||||
|
const configNode = document.getElementById('email-preview-config');
|
||||||
|
|
||||||
|
if (!editor || !preview || !configNode) return;
|
||||||
|
|
||||||
|
const config = JSON.parse(configNode.textContent);
|
||||||
|
const contentSlotPattern = /\{\{\s*content_for_layout\s*\}\}/g;
|
||||||
|
const replacements = [
|
||||||
|
[/\{\{\s*global_config\[['"]BRAND_NAME['"]\]\s*\}\}/g, config.brandName],
|
||||||
|
[/\{\{\s*global_config\[['"]BRAND_URL['"]\]\s*\}\}/g, config.brandUrl],
|
||||||
|
[/\{\{\s*account\.name\s*\}\}/g, config.accountName],
|
||||||
|
];
|
||||||
|
|
||||||
|
const renderTemplateVariables = value => replacements.reduce((body, [pattern, replacement]) => body.replace(pattern, replacement || ''), value || '');
|
||||||
|
|
||||||
|
const renderPreview = () => {
|
||||||
|
const renderedBody = renderTemplateVariables(editor.value);
|
||||||
|
preview.srcdoc = config.templateType === 'layout'
|
||||||
|
? renderedBody.replace(contentSlotPattern, config.previewContent)
|
||||||
|
: renderTemplateVariables(config.defaultLayout).replace(contentSlotPattern, renderedBody);
|
||||||
|
};
|
||||||
|
|
||||||
|
editor.addEventListener('input', renderPreview);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initializeEmailTemplateEditor);
|
||||||
|
} else {
|
||||||
|
initializeEmailTemplateEditor();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
@@ -2,6 +2,7 @@ require 'rails_helper'
|
|||||||
|
|
||||||
RSpec.describe 'Super Admin Application Config API', type: :request do
|
RSpec.describe 'Super Admin Application Config API', type: :request do
|
||||||
let(:super_admin) { create(:super_admin) }
|
let(:super_admin) { create(:super_admin) }
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
|
||||||
describe 'GET /super_admin/app_config' do
|
describe 'GET /super_admin/app_config' do
|
||||||
context 'when it is an unauthenticated super admin' do
|
context 'when it is an unauthenticated super admin' do
|
||||||
@@ -12,7 +13,13 @@ RSpec.describe 'Super Admin Application Config API', type: :request do
|
|||||||
end
|
end
|
||||||
|
|
||||||
context 'when it is an authenticated super admin' do
|
context 'when it is an authenticated super admin' do
|
||||||
let!(:config) { create(:installation_config, { name: 'FB_APP_ID', value: 'TESTVALUE' }) }
|
let!(:config) do
|
||||||
|
InstallationConfig.where(name: 'FB_APP_ID').first_or_initialize.tap do |installation_config|
|
||||||
|
installation_config.value = 'TESTVALUE'
|
||||||
|
installation_config.locked = false
|
||||||
|
installation_config.save!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it 'shows the app_config page' do
|
it 'shows the app_config page' do
|
||||||
sign_in(super_admin, scope: :super_admin)
|
sign_in(super_admin, scope: :super_admin)
|
||||||
@@ -20,6 +27,32 @@ RSpec.describe 'Super Admin Application Config API', type: :request do
|
|||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
expect(response.body).to include(config.value)
|
expect(response.body).to include(config.value)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'shows email template customization under custom branding' do
|
||||||
|
allow(ChatwootHub).to receive(:pricing_plan).and_return('enterprise')
|
||||||
|
|
||||||
|
sign_in(super_admin, scope: :super_admin)
|
||||||
|
get '/super_admin/app_config?config=custom_branding'
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(response.body).to include('Email templates')
|
||||||
|
expect(response.body).to include('Customize the notification email layout for this brand')
|
||||||
|
expect(response.body).to include('Manage')
|
||||||
|
expect(response.body).not_to include('Shape the brand customers see across Chatwoot and email')
|
||||||
|
expect(response.body).to include('/super_admin/email_layout/edit')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows email template customization on account details' do
|
||||||
|
allow(ChatwootHub).to receive(:pricing_plan).and_return('enterprise')
|
||||||
|
|
||||||
|
sign_in(super_admin, scope: :super_admin)
|
||||||
|
get "/super_admin/accounts/#{account.id}"
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(response.body).to include('Account Email Templates')
|
||||||
|
expect(response.body).to include('Customize account email templates')
|
||||||
|
expect(response.body).to include(edit_super_admin_account_email_layout_path(account))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Super Admin Email Layouts API', type: :request do
|
||||||
|
let(:super_admin) { create(:super_admin) }
|
||||||
|
let(:account) { create(:account, name: 'Acme Inc') }
|
||||||
|
|
||||||
|
describe 'GET /super_admin/email_layout/edit' do
|
||||||
|
context 'when it is an unauthenticated super admin' do
|
||||||
|
it 'returns unauthorized' do
|
||||||
|
get '/super_admin/email_layout/edit'
|
||||||
|
expect(response).to have_http_status(:redirect)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when it is an authenticated super admin' do
|
||||||
|
it 'shows the email layout page' do
|
||||||
|
allow(ChatwootHub).to receive(:pricing_plan).and_return('enterprise')
|
||||||
|
sign_in(super_admin, scope: :super_admin)
|
||||||
|
|
||||||
|
get '/super_admin/email_layout/edit'
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(response.body).to include('Configure Settings - Email Templates')
|
||||||
|
expect(response.body).to include('Create new template')
|
||||||
|
expect(response.body).to include('<th class="cell-label cell-label--string" scope="col">Type</th>')
|
||||||
|
expect(response.body).to include('<th class="cell-label cell-label--string" scope="col">Template</th>')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows the editor when creating a new template override' do
|
||||||
|
allow(ChatwootHub).to receive(:pricing_plan).and_return('enterprise')
|
||||||
|
sign_in(super_admin, scope: :super_admin)
|
||||||
|
|
||||||
|
get '/super_admin/email_layout/edit', params: { new: 1, template_type: 'layout', template_name: 'base', locale: 'en' }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(response.body).to include('Template details')
|
||||||
|
expect(response.body).to include('email-template-selector-form')
|
||||||
|
expect(response.body).to include('Type')
|
||||||
|
expect(response.body).to include('Language')
|
||||||
|
expect(response.body).to include('Inherits default template')
|
||||||
|
expect(response.body).to include('Email layout preview')
|
||||||
|
expect(response.body).to include('Variables')
|
||||||
|
expect(response.body).to include('{{ content_for_layout }}')
|
||||||
|
expect(response.body).to include('global_config['BRAND_NAME']')
|
||||||
|
expect(response.body).to include('global_config['BRAND_URL']')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows the account email layout page' do
|
||||||
|
allow(ChatwootHub).to receive(:pricing_plan).and_return('enterprise')
|
||||||
|
sign_in(super_admin, scope: :super_admin)
|
||||||
|
|
||||||
|
get "/super_admin/accounts/#{account.id}/email_layout/edit"
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(response.body).to include('Configure Settings - Acme Inc Email Templates')
|
||||||
|
expect(response.body).to include('Create new template')
|
||||||
|
expect(response.body).to include('<th class="cell-label cell-label--string" scope="col">Type</th>')
|
||||||
|
expect(response.body).to include('<th class="cell-label cell-label--string" scope="col">Template</th>')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'lists configured account email templates' do
|
||||||
|
allow(ChatwootHub).to receive(:pricing_plan).and_return('enterprise')
|
||||||
|
create(
|
||||||
|
:email_template,
|
||||||
|
name: 'agent_notifications/conversation_notifications_mailer/conversation_assignment',
|
||||||
|
body: 'Assigned',
|
||||||
|
template_type: :content,
|
||||||
|
locale: :fr,
|
||||||
|
account: account
|
||||||
|
)
|
||||||
|
sign_in(super_admin, scope: :super_admin)
|
||||||
|
|
||||||
|
get "/super_admin/accounts/#{account.id}/email_layout/edit"
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(response.body).to include('Create new template')
|
||||||
|
expect(response.body).to include('Content')
|
||||||
|
expect(response.body).to include('FR')
|
||||||
|
expect(response.body).to include('Agent notifications/conversation notifications mailer/conversation assignment')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows the account email layout editor when creating a new template override' do
|
||||||
|
allow(ChatwootHub).to receive(:pricing_plan).and_return('enterprise')
|
||||||
|
sign_in(super_admin, scope: :super_admin)
|
||||||
|
|
||||||
|
get "/super_admin/accounts/#{account.id}/email_layout/edit",
|
||||||
|
params: { new: 1, template_type: 'layout', template_name: 'base', locale: 'en' }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(response.body).to include('Template details')
|
||||||
|
expect(response.body).to include('Acme Inc')
|
||||||
|
expect(response.body).to include('{{ account.name }}')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows delete action for configured account email template overrides' do
|
||||||
|
allow(ChatwootHub).to receive(:pricing_plan).and_return('enterprise')
|
||||||
|
create(:email_template, name: 'base', body: '{{ content_for_layout }}', template_type: :layout, locale: :en, account: account)
|
||||||
|
sign_in(super_admin, scope: :super_admin)
|
||||||
|
|
||||||
|
get "/super_admin/accounts/#{account.id}/email_layout/edit", params: { template_type: 'layout', template_name: 'base', locale: 'en' }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(response.body).to include('Destroy')
|
||||||
|
expect(response.body).to include('button button--danger')
|
||||||
|
expect(response.body).to include('Are you sure?')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'supports selecting content templates by locale' do
|
||||||
|
allow(ChatwootHub).to receive(:pricing_plan).and_return('enterprise')
|
||||||
|
sign_in(super_admin, scope: :super_admin)
|
||||||
|
|
||||||
|
get "/super_admin/accounts/#{account.id}/email_layout/edit",
|
||||||
|
params: {
|
||||||
|
template_type: 'content',
|
||||||
|
template_name: 'agent_notifications/conversation_notifications_mailer/conversation_assignment',
|
||||||
|
locale: 'fr'
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(response.body).to include('Content')
|
||||||
|
expect(response.body).to include('conversation assignment')
|
||||||
|
expect(response.body).to include('FR')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'blocks access on community plan' do
|
||||||
|
allow(ChatwootHub).to receive(:pricing_plan).and_return('community')
|
||||||
|
sign_in(super_admin, scope: :super_admin)
|
||||||
|
|
||||||
|
get '/super_admin/email_layout/edit'
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:redirect)
|
||||||
|
expect(response).to redirect_to(root_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'PATCH /super_admin/email_layout' do
|
||||||
|
context 'when it is an unauthenticated super admin' do
|
||||||
|
it 'returns unauthorized' do
|
||||||
|
patch '/super_admin/email_layout', params: { email_layout: { body: '{{ content_for_layout }}' } }
|
||||||
|
expect(response).to have_http_status(:redirect)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when it is an authenticated super admin' do
|
||||||
|
before do
|
||||||
|
allow(ChatwootHub).to receive(:pricing_plan).and_return('enterprise')
|
||||||
|
sign_in(super_admin, scope: :super_admin)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a layout override' do
|
||||||
|
body = '<html><body>{{ content_for_layout }}</body></html>'
|
||||||
|
|
||||||
|
patch '/super_admin/email_layout', params: { email_layout: { body: body } }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:found)
|
||||||
|
expect(response).to redirect_to(edit_super_admin_email_layout_path(template_type: 'layout', template_name: 'base', locale: 'en'))
|
||||||
|
|
||||||
|
template = EmailTemplate.find_by(name: 'base', account: nil)
|
||||||
|
expect(template).to be_present
|
||||||
|
expect(template.template_type).to eq('layout')
|
||||||
|
expect(template.body).to eq(body)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates an account layout override' do
|
||||||
|
body = '<html><body>{{ content_for_layout }} {{ account.name }}</body></html>'
|
||||||
|
|
||||||
|
patch "/super_admin/accounts/#{account.id}/email_layout", params: { email_layout: { body: body } }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:found)
|
||||||
|
expect(response).to redirect_to(edit_super_admin_account_email_layout_path(account, template_type: 'layout', template_name: 'base',
|
||||||
|
locale: 'en'))
|
||||||
|
|
||||||
|
template = EmailTemplate.find_by(name: 'base', account: account)
|
||||||
|
expect(template).to be_present
|
||||||
|
expect(template.template_type).to eq('layout')
|
||||||
|
expect(template.body).to eq(body)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns validation error when content_for_layout is missing' do
|
||||||
|
patch '/super_admin/email_layout', params: { email_layout: { body: '<html><body>No slot</body></html>' } }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
expect(response.body).to include('Layout must include')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns validation error for invalid liquid syntax' do
|
||||||
|
patch '/super_admin/email_layout', params: { email_layout: { body: '{{ content_for_layout }} {{ invalid ' } }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
expect(response.body).to include('Liquid syntax error')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'DELETE /super_admin/email_layout' do
|
||||||
|
context 'when it is an unauthenticated super admin' do
|
||||||
|
it 'returns unauthorized' do
|
||||||
|
delete '/super_admin/email_layout'
|
||||||
|
expect(response).to have_http_status(:redirect)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when it is an authenticated super admin' do
|
||||||
|
it 'removes custom layout override' do
|
||||||
|
allow(ChatwootHub).to receive(:pricing_plan).and_return('enterprise')
|
||||||
|
create(:email_template, name: 'base', body: '{{ content_for_layout }}', template_type: :layout, account: nil)
|
||||||
|
sign_in(super_admin, scope: :super_admin)
|
||||||
|
|
||||||
|
delete '/super_admin/email_layout'
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:found)
|
||||||
|
expect(response).to redirect_to(edit_super_admin_email_layout_path)
|
||||||
|
expect(EmailTemplate.find_by(name: 'base', account: nil)).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes account layout override' do
|
||||||
|
allow(ChatwootHub).to receive(:pricing_plan).and_return('enterprise')
|
||||||
|
create(:email_template, name: 'base', body: '{{ content_for_layout }}', template_type: :layout, account: account)
|
||||||
|
sign_in(super_admin, scope: :super_admin)
|
||||||
|
|
||||||
|
delete "/super_admin/accounts/#{account.id}/email_layout"
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:found)
|
||||||
|
expect(response).to redirect_to(edit_super_admin_account_email_layout_path(account))
|
||||||
|
expect(EmailTemplate.find_by(name: 'base', account: account)).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user