Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7edbe3a2b | ||
|
|
bac921133a |
@@ -11,7 +11,7 @@ saml:
|
||||
enterprise: true
|
||||
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') %>
|
||||
icon: 'icon-paint-brush-line'
|
||||
config_key: 'custom_branding'
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
module SuperAdmin::NavigationHelper
|
||||
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
|
||||
|
||||
def settings_pages
|
||||
|
||||
@@ -20,7 +20,7 @@ class EmailTemplate < ApplicationRecord
|
||||
enum :template_type, { layout: 0, content: 1 }
|
||||
belongs_to :account, optional: true
|
||||
|
||||
validates :name, uniqueness: { scope: :account }
|
||||
validates :name, uniqueness: { scope: [:account, :template_type, :locale] }
|
||||
|
||||
def self.resolver(options = {})
|
||||
::EmailTemplates::DbResolverService.using self, options
|
||||
|
||||
@@ -35,6 +35,8 @@ class ::EmailTemplates::DbResolverService < ActionView::Resolver
|
||||
def find_templates(name, prefix, partial, *_args)
|
||||
@template_name = name
|
||||
@template_type = prefix.include?('layout') ? 'layout' : 'content'
|
||||
@_prefix = prefix
|
||||
@_details = _args.first || {}
|
||||
@db_template = find_db_template
|
||||
|
||||
return [] if @db_template.blank?
|
||||
@@ -60,11 +62,23 @@ class ::EmailTemplates::DbResolverService < ActionView::Resolver
|
||||
def find_account_template
|
||||
return unless Current.account
|
||||
|
||||
@@model.find_by(name: @template_name, template_type: @template_type, account: Current.account)
|
||||
find_template_for(Current.account)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
# Build path with eventual prefix
|
||||
@@ -72,6 +86,12 @@ class ::EmailTemplates::DbResolverService < ActionView::Resolver
|
||||
prefix.present? ? "#{prefix}/#{@template_name}" : @template_name
|
||||
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
|
||||
# params path: path/to/file.ext partial: true/false
|
||||
# the function appends _to make the file name _file.ext if partial: true
|
||||
|
||||
@@ -14,66 +14,72 @@
|
||||
}
|
||||
</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| %>
|
||||
<% @allowed_configs.each do |key| %>
|
||||
<div class="flex mb-8">
|
||||
<div class="field-unit__label">
|
||||
<%= form.label "app_config[#{key}]", @installation_configs[key]&.dig('display_title') || key %>
|
||||
</div>
|
||||
<div class="-mt-2 field-unit__field ">
|
||||
<% if @installation_configs[key]&.dig('type') == 'boolean' %>
|
||||
<%= form.select "app_config[#{key}]",
|
||||
[["True", true], ["False", false]],
|
||||
{ selected: ActiveModel::Type::Boolean.new.cast(@app_config[key]) },
|
||||
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"
|
||||
<% @allowed_configs.each do |key| %>
|
||||
<div class="flex mb-8">
|
||||
<div class="field-unit__label">
|
||||
<%= form.label "app_config[#{key}]", @installation_configs[key]&.dig('display_title') || key %>
|
||||
</div>
|
||||
<div class="-mt-2 field-unit__field ">
|
||||
<% if @installation_configs[key]&.dig('type') == 'boolean' %>
|
||||
<%= form.select "app_config[#{key}]",
|
||||
[["True", true], ["False", false]],
|
||||
{ selected: ActiveModel::Type::Boolean.new.cast(@app_config[key]) },
|
||||
class: "mt-2 border border-slate-100 p-1 rounded-md"
|
||||
%>
|
||||
<button
|
||||
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"
|
||||
data-target="app_config_<%= key %>"
|
||||
>
|
||||
<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">
|
||||
<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"/>
|
||||
</svg>
|
||||
</button>
|
||||
</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 %>
|
||||
<% 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
|
||||
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"
|
||||
data-target="app_config_<%= key %>"
|
||||
>
|
||||
<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">
|
||||
<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"/>
|
||||
</svg>
|
||||
</button>
|
||||
</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>
|
||||
<% end %>
|
||||
|
||||
<% end %>
|
||||
|
||||
<div class="form-actions">
|
||||
<%= form.submit "Submit" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
</section>
|
||||
|
||||
<% content_for :javascript do %>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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 %>
|
||||
<svg width="16" height="16"><use xlink:href="#<%= icon %>" /></svg>
|
||||
<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">
|
||||
<%= render partial: "nav_item", locals: { icon: 'icon-grid-line', url: super_admin_root_url, label: 'Dashboard' } %>
|
||||
<% 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? %>
|
||||
<% 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: {
|
||||
icon: sidebar_icons[resource.resource.to_sym],
|
||||
url: resource_index_route(resource),
|
||||
url: resource_url,
|
||||
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
|
||||
post :seed, on: :member
|
||||
post :reset_cache, on: :member
|
||||
resource :email_layout, only: [:edit, :update, :destroy] if ChatwootApp.enterprise?
|
||||
end
|
||||
resources :users, only: [:index, :new, :create, :show, :edit, :update, :destroy] do
|
||||
delete :avatar, on: :member, action: :destroy_avatar
|
||||
@@ -702,6 +703,7 @@ Rails.application.routes.draw do
|
||||
resource :settings, only: [:show] do
|
||||
get :refresh, on: :collection
|
||||
end
|
||||
resource :email_layout, only: [:edit, :update, :destroy] if ChatwootApp.enterprise?
|
||||
|
||||
# resources that doesn't appear in primary navigation in super admin
|
||||
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.datetime "created_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
|
||||
|
||||
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>
|
||||
</div>
|
||||
<% 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>
|
||||
|
||||
@@ -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
|
||||
let(:super_admin) { create(:super_admin) }
|
||||
let(:account) { create(:account) }
|
||||
|
||||
describe 'GET /super_admin/app_config' 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
|
||||
|
||||
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
|
||||
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.body).to include(config.value)
|
||||
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
|
||||
|
||||
|
||||
@@ -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