diff --git a/.rubocop_todo.rspec.yml b/.rubocop_todo.rspec.yml
index 638f35dd9..4e2087e32 100644
--- a/.rubocop_todo.rspec.yml
+++ b/.rubocop_todo.rspec.yml
@@ -91,6 +91,7 @@ RSpec/FilePath:
- 'spec/db/migrate/issue_2460_fix_corrupted_twitter_ids_spec.rb'
- 'spec/db/migrate/issue_2715_fix_broken_twitter_urls_spec.rb'
- 'spec/jobs/issue_2715_fix_broken_twitter_urls_job_spec.rb'
+ - 'spec/db/migrate/issue_2867_footer_header_public_link_spec.rb'
- 'spec/lib/import/base_factory_spec.rb'
# Offense count: 60
diff --git a/app/assets/javascripts/app/controllers/_manage/knowledge_base.coffee b/app/assets/javascripts/app/controllers/_manage/knowledge_base.coffee
index 6cdfcb82b..da290cb66 100644
--- a/app/assets/javascripts/app/controllers/_manage/knowledge_base.coffee
+++ b/app/assets/javascripts/app/controllers/_manage/knowledge_base.coffee
@@ -109,7 +109,7 @@ class App.ManageKnowledgeBase extends App.ControllerTabs
},{
name: 'Public Menu'
target: 'public_menu'
- controller: App.KnowledgeBasePublicMenuForm
+ controller: App.KnowledgeBasePublicMenuManager
params: _.extend({}, params, { screen: 'public_menu' })
},{
name: 'Delete'
diff --git a/app/assets/javascripts/app/controllers/knowledge_base/public_menu_form.coffee b/app/assets/javascripts/app/controllers/knowledge_base/public_menu_form.coffee
index f30ad420a..99d11fd58 100644
--- a/app/assets/javascripts/app/controllers/knowledge_base/public_menu_form.coffee
+++ b/app/assets/javascripts/app/controllers/knowledge_base/public_menu_form.coffee
@@ -1,17 +1,69 @@
-class App.KnowledgeBasePublicMenuForm extends App.Controller
- events:
- 'show.bs.tab': 'willShow'
+class App.KnowledgeBasePublicMenuForm extends App.ControllerModal
+ autoFocusOnFirstInput: false
+ includeForm: true
- willShow: ->
- @el.empty()
+ constructor: (params) ->
+ @formItems = []
+ @head = params.location.headline
+ super
- for kb_locale in App.KnowledgeBase.find(@knowledge_base_id).kb_locales()
- menu_items = App.KnowledgeBaseMenuItem.using_kb_locale(kb_locale)
+ formParams: =>
+ @formItems.map (elem) -> elem.buildData()
- form_item = new App.KnowledgeBasePublicMenuFormItem(
- knowledge_base_id: @knowledge_base_id,
- kb_locale: kb_locale,
- menu_items: menu_items
- )
+ content: ->
+ @formItems = App.KnowledgeBase
+ .find(@knowledge_base_id)
+ .kb_locales()
+ .map (kb_locale) =>
+ menu_items = App.KnowledgeBaseMenuItem.using_kb_locale_location(kb_locale, @location.identifier)
- @el.append form_item.el
+ new App.KnowledgeBasePublicMenuFormItem(
+ parent: @,
+ knowledge_base_id: @knowledge_base_id,
+ location: @location.identifier,
+ kb_locale: kb_locale,
+ menu_items: menu_items
+ )
+
+ @formItems.map (elem) -> elem.el
+
+ hasError: ->
+ @formItems
+ .map (elem) -> elem.hasError()
+ .filter((elem) -> elem)
+ .pop()
+
+ onSubmit: (e) ->
+ @preventDefaultAndStopPropagation(e)
+
+ if error = @hasError()
+ @showAlert(error)
+ return
+
+ @clearAlerts()
+ @formItems.forEach (elem) -> elem.toggleUserInteraction(false)
+
+ kb = App.KnowledgeBase.find(@knowledge_base_id)
+
+ @ajax(
+ id: 'update_menu_items'
+ type: 'PATCH'
+ url: kb.manageUrl('update_menu_items')
+ data: JSON.stringify(menu_items_sets: @formParams())
+ processData: true
+ success: @onSuccess
+ error: @onError
+ )
+
+ onSuccess: (data, status, xhr) =>
+ for formItem in @formItems
+ for menuItem in App.KnowledgeBaseMenuItem.using_kb_locale_location(formItem.kb_locale, formItem.location)
+ menuItem.remove(clear: true)
+
+ App.Collection.loadAssets(data.assets)
+ App.KnowledgeBaseMenuItem.trigger('kb_data_change_loaded')
+ @close()
+
+ onError: (xhr) =>
+ @showAlert(xhr.responseJSON?.error_human || 'Couldn\'t save changes')
+ @formItems.forEach (elem) -> elem.toggleUserInteraction(true)
diff --git a/app/assets/javascripts/app/controllers/knowledge_base/public_menu_form_item.coffee b/app/assets/javascripts/app/controllers/knowledge_base/public_menu_form_item.coffee
index 1c403cabf..96e63c7de 100644
--- a/app/assets/javascripts/app/controllers/knowledge_base/public_menu_form_item.coffee
+++ b/app/assets/javascripts/app/controllers/knowledge_base/public_menu_form_item.coffee
@@ -3,7 +3,6 @@ class App.KnowledgeBasePublicMenuFormItem extends App.Controller
'click .js-add': 'add'
'click .js-remove': 'remove'
'input input': 'input'
- 'submit form': 'submit'
elements:
'.js-alert': 'alert'
@@ -14,30 +13,29 @@ class App.KnowledgeBasePublicMenuFormItem extends App.Controller
render: ->
@html App.view('knowledge_base/public_menu_form_item')(
- kb_locale_id: @kb_locale.id
- rows: @menu_items
- title: @kb_locale.systemLocale().name
+ rows: @menu_items
+ title: @kb_locale.systemLocale().name
)
@applySortable()
applySortable: ->
dndOptions =
- tolerance: 'pointer'
- distance: 15
- opacity: 0.6
- items: 'tr.sortable'
- start: (e, ui) ->
+ tolerance: 'pointer'
+ distance: 15
+ opacity: 0.6
+ items: 'tr.sortable'
+ start: (e, ui) ->
ui.placeholder.height( ui.item.height() )
- helper: (e, tr) ->
+ helper: (e, tr) ->
originals = tr.children()
helper = tr
helper.children().each (index, el) ->
# Set helper cell sizes to match the original sizes
$(@).width( originals.eq(index).width() )
return helper
- update: @dndCallback
- stop: (e, ui) ->
+ update: @dndCallback
+ stop: (e, ui) ->
ui.item.children().each (index, element) ->
element.style.width = ''
@@ -66,13 +64,14 @@ class App.KnowledgeBasePublicMenuFormItem extends App.Controller
}
{
- kb_locale_id: @$('form').data('kb-locale-id'),
- menu_items: items
+ kb_locale_id: @kb_locale.id,
+ location: @location,
+ menu_items: items
}
input: ->
- if @validateForm(false)
- @hideAlert()
+ if !@hasError()
+ @parent.clearAlerts()
add: ->
el = App.view('knowledge_base/public_menu_form_item_row')()
@@ -88,57 +87,14 @@ class App.KnowledgeBasePublicMenuFormItem extends App.Controller
else
row.remove()
- showAlert: (message) ->
- translated = App.i18n.translatePlain(message)
-
- @alert
- .text(translated)
- .removeClass('hidden')
-
- hideAlert: ->
- @alert.addClass('hidden')
-
- emptyFields: ->
+ findEmptyFields: ->
@$('tr.sortable:not(.js-deleted)')
.find('input[data-name]')
.toArray()
.filter (elem) -> $(elem).val().length == 0
- validateForm: (showAlert = true) ->
- if @emptyFields().length == 0
- return true
+ hasError: ->
+ if @findEmptyFields().length == 0
+ return false
- if showAlert
- @showAlert('Please fill in all fields')
-
- false
-
- submit: (e) ->
- @preventDefaultAndStopPropagation(e)
-
- if !@validateForm()
- return
-
- @hideAlert()
- @toggleUserInteraction(false)
-
- kb = App.KnowledgeBase.find(@knowledge_base_id)
-
- @ajax(
- id: 'update_menu_items'
- type: 'PATCH'
- url: kb.manageUrl('update_menu_items')
- data: JSON.stringify(@buildData())
- processData: true
- success: (data, status, xhr) =>
- for menu_item in App.KnowledgeBaseMenuItem.using_kb_locale(@kb_locale)
- menu_item.remove(clear: true)
-
- App.Collection.loadAssets(data.assets)
-
- @menu_items = App.KnowledgeBaseMenuItem.using_kb_locale(@kb_locale)
- @render()
- error: (xhr) =>
- @showAlert(xhr.responseJSON?.error_human || 'Couldn\'t save changes')
- @toggleUserInteraction(true)
- )
+ 'Please fill in all fields'
diff --git a/app/assets/javascripts/app/controllers/knowledge_base/public_menu_manager.coffee b/app/assets/javascripts/app/controllers/knowledge_base/public_menu_manager.coffee
new file mode 100644
index 000000000..879497e6d
--- /dev/null
+++ b/app/assets/javascripts/app/controllers/knowledge_base/public_menu_manager.coffee
@@ -0,0 +1,49 @@
+class App.KnowledgeBasePublicMenuManager extends App.Controller
+ events:
+ 'show.bs.tab': 'willShow'
+ 'click .js-edit': 'edit'
+
+ constructor: ->
+ super
+
+ @listenTo App.KnowledgeBaseMenuItem, 'kb_data_change_loaded', =>
+ @render()
+
+ willShow: ->
+ @render()
+
+ render: ->
+ kb = App.KnowledgeBase.find(@knowledge_base_id)
+
+ @html App.view('knowledge_base/public_menu_manager')(
+ locations: @locations(),
+ locales: kb.kb_locales()
+ )
+
+ locations: ->
+ kb = App.KnowledgeBase.find(@knowledge_base_id)
+
+ [
+ {
+ headline: 'Header menu',
+ identifier: 'header',
+ color: kb.color_header
+ },
+ {
+ headline: 'Footer menu',
+ identifier: 'footer'
+ }
+ ]
+
+
+ edit: (e) =>
+ @preventDefaultAndStopPropagation(e)
+
+ identifier = $(e.target).data('target-location')
+ location = _.find @locations(), (elem) -> elem.identifier == identifier
+
+ new App.KnowledgeBasePublicMenuForm(
+ location: location,
+ knowledge_base_id: @knowledge_base_id
+ container: @el.closest('.main')
+ )
diff --git a/app/assets/javascripts/app/models/knowledge_base_menu_item.coffee b/app/assets/javascripts/app/models/knowledge_base_menu_item.coffee
index acb7acfa9..d8977d16a 100644
--- a/app/assets/javascripts/app/models/knowledge_base_menu_item.coffee
+++ b/app/assets/javascripts/app/models/knowledge_base_menu_item.coffee
@@ -5,3 +5,8 @@ class App.KnowledgeBaseMenuItem extends App.Model
items = @findAllByAttribute('kb_locale_id', kb_locale.id)
items.sort( (a, b) -> if a.position < b.position then -1 else 1)
items
+
+ @using_kb_locale_location: (kb_locale, location) ->
+ items = @all().filter (elem) -> elem.kb_locale_id is kb_locale.id and elem.location is location
+ items.sort( (a, b) -> if a.position < b.position then -1 else 1)
+ items
diff --git a/app/assets/javascripts/app/views/knowledge_base/public_menu_form_item.jst.eco b/app/assets/javascripts/app/views/knowledge_base/public_menu_form_item.jst.eco
index 96d5e0b4e..f9b320370 100644
--- a/app/assets/javascripts/app/views/knowledge_base/public_menu_form_item.jst.eco
+++ b/app/assets/javascripts/app/views/knowledge_base/public_menu_form_item.jst.eco
@@ -1,4 +1,4 @@
-
+
diff --git a/app/assets/javascripts/app/views/knowledge_base/public_menu_manager.jst.eco b/app/assets/javascripts/app/views/knowledge_base/public_menu_manager.jst.eco
new file mode 100644
index 000000000..3ccb6b454
--- /dev/null
+++ b/app/assets/javascripts/app/views/knowledge_base/public_menu_manager.jst.eco
@@ -0,0 +1,40 @@
+
+ <%- @T 'Public Menu' %>
+
+
+
+ <%- @T 'Here you can add further links to your public FAQ page, which will be displayed either in the header or footer.' %>
+
+
+<% for location in @locations: %>
+
+<% end %>
diff --git a/app/assets/stylesheets/zammad.scss b/app/assets/stylesheets/zammad.scss
index 0efd396ea..c0001482a 100644
--- a/app/assets/stylesheets/zammad.scss
+++ b/app/assets/stylesheets/zammad.scss
@@ -2472,6 +2472,36 @@ input.has-error {
}
}
+.kb-menu-preview {
+ margin-bottom: 1em;
+
+ &-container {
+ display: flex;
+ justify-content: flex-end;
+ border: 1px solid hsl(213,14%,91%);
+
+ &--footer {
+ justify-content: center;
+ }
+ }
+
+ a, span {
+ font-size: 14px;
+ padding: .5em 1em;
+ white-space: nowrap;
+ text-decoration: none;
+ line-height: 2em;
+ }
+
+ a {
+ color: hsl(206,8%,50%);
+ }
+
+ .label {
+ text-transform: none;
+ }
+}
+
.modified-icon {
position: relative;
line-height: 1;
@@ -11684,3 +11714,11 @@ span.is-disabled {
width: 100%;
height: 100%;
}
+
+.btn-manage-public-menu-edit {
+ margin-top: 0
+}
+
+.kb-menu-settings-entry {
+ margin-bottom: 12px
+}
diff --git a/app/controllers/knowledge_base/manage_controller.rb b/app/controllers/knowledge_base/manage_controller.rb
index 16a181f14..799d732a2 100644
--- a/app/controllers/knowledge_base/manage_controller.rb
+++ b/app/controllers/knowledge_base/manage_controller.rb
@@ -31,13 +31,10 @@ class KnowledgeBase::ManageController < KnowledgeBase::BaseController
def update_menu_items
kb = KnowledgeBase.find params[:id]
- kb_locale = kb.kb_locales.find params[:kb_locale_id]
- KnowledgeBase::MenuItemUpdateAction
- .new(kb_locale, params[:menu_items])
- .perform!
+ affected_items = KnowledgeBase::MenuItemUpdateAction.update_using_params! kb, params_for_permission[:menu_items_sets]
- render json: { assets: ApplicationModel::CanAssets.reduce(kb_locale.menu_items.reload, {}) }
+ render json: { assets: ApplicationModel::CanAssets.reduce(affected_items || [], {}) }
end
def destroy
diff --git a/app/controllers/knowledge_base/public/base_controller.rb b/app/controllers/knowledge_base/public/base_controller.rb
index ca1f17010..8f1deeb3d 100644
--- a/app/controllers/knowledge_base/public/base_controller.rb
+++ b/app/controllers/knowledge_base/public/base_controller.rb
@@ -24,9 +24,7 @@ class KnowledgeBase::Public::BaseController < ApplicationController
end
def menu_items
- @menu_items ||= KnowledgeBase::MenuItem
- .sorted
- .using_locale(guess_locale_via_uri || filter_primary_kb_locale)
+ @menu_items ||= KnowledgeBase::MenuItem.using_locale(guess_locale_via_uri || filter_primary_kb_locale)
end
def system_locale_via_uri
diff --git a/app/models/knowledge_base/menu_item.rb b/app/models/knowledge_base/menu_item.rb
index 81ccd6a3a..8c89867c4 100644
--- a/app/models/knowledge_base/menu_item.rb
+++ b/app/models/knowledge_base/menu_item.rb
@@ -1,17 +1,24 @@
class KnowledgeBase::MenuItem < ApplicationModel
belongs_to :kb_locale, class_name: 'KnowledgeBase::Locale', inverse_of: :menu_items, touch: true
- validates :title, presence: true, length: { maximum: 100 }
- validates :url, presence: true, length: { maximum: 100 }
+ validates :title, presence: true, length: { maximum: 100 }
+ validates :url, presence: true, length: { maximum: 500 }
+ validates :location, presence: true, inclusion: { in: %w[header footer] }
- acts_as_list scope: :kb_locale, top_of_list: 0
+ acts_as_list scope: %i[kb_locale_id location], top_of_list: 0
- scope :sorted, -> { order(position: :asc) }
- scope :using_locale, ->(locale) { locale.present? ? joins(:kb_locale).where(knowledge_base_locales: { system_locale_id: locale.id } ) : none }
+ scope :sorted, -> { order(position: :asc) }
+ scope :using_locale, ->(locale) { locale.present? ? joins(:kb_locale).where(knowledge_base_locales: { system_locale_id: locale.id } ) : none }
+ scope :location, ->(location) { sorted.where(location: location) }
+
+ scope :location_header, -> { location(:header) }
+ scope :location_footer, -> { location(:footer) }
private
def add_protocol_prefix
+ return if url.blank?
+
url.strip!
return if url.match? %r{^\S+\:\/\/}
diff --git a/app/views/layouts/knowledge_base.html.erb b/app/views/layouts/knowledge_base.html.erb
index ede5e249d..2a1df3589 100644
--- a/app/views/layouts/knowledge_base.html.erb
+++ b/app/views/layouts/knowledge_base.html.erb
@@ -25,7 +25,7 @@
<% end %>
@@ -51,6 +51,13 @@
<%= @knowledge_base.translation.footer_note %>
+
+
+
<%= system_locale_via_uri.name %>
diff --git a/db/migrate/20190531180304_initialize_knowledge_base.rb b/db/migrate/20190531180304_initialize_knowledge_base.rb
index a456737c9..70ad68a54 100644
--- a/db/migrate/20190531180304_initialize_knowledge_base.rb
+++ b/db/migrate/20190531180304_initialize_knowledge_base.rb
@@ -92,6 +92,7 @@ class InitializeKnowledgeBase < ActiveRecord::Migration[5.0]
create_table :knowledge_base_menu_items do |t|
t.references :kb_locale, null: false, foreign_key: { to_table: :knowledge_base_locales, on_delete: :cascade }
+ t.string :location, null: false, index: true
t.integer :position, null: false, index: true
t.string :title, null: false, limit: 100
t.string :url, null: false, limit: 500
diff --git a/db/migrate/20190918114553_issue_2867_footer_header_public_link.rb b/db/migrate/20190918114553_issue_2867_footer_header_public_link.rb
new file mode 100644
index 000000000..78749ed4c
--- /dev/null
+++ b/db/migrate/20190918114553_issue_2867_footer_header_public_link.rb
@@ -0,0 +1,14 @@
+class Issue2867FooterHeaderPublicLink < ActiveRecord::Migration[5.2]
+ def up
+ # return if it's a new setup
+ return if !Setting.find_by(name: 'system_init_done')
+
+ add_column :knowledge_base_menu_items, :location, :string, null: false, default: 'header'
+ add_index :knowledge_base_menu_items, :location
+ change_column_default :knowledge_base_menu_items, :location, nil
+ end
+
+ def down
+ remove_column :knowledge_base_menu_items, :location
+ end
+end
diff --git a/lib/knowledge_base/menu_item_update_action.rb b/lib/knowledge_base/menu_item_update_action.rb
index 0589190f6..a85ddf059 100644
--- a/lib/knowledge_base/menu_item_update_action.rb
+++ b/lib/knowledge_base/menu_item_update_action.rb
@@ -1,10 +1,15 @@
class KnowledgeBase
class MenuItemUpdateAction
- def initialize(kb_locale, menu_items_data)
+ def initialize(kb_locale, location, menu_items_data)
@kb_locale = kb_locale
+ @location = location
@menu_items_data = menu_items_data
end
+ def scope
+ @kb_locale.menu_items.location(@location)
+ end
+
def perform!
raise_unprocessable unless all_ids_present?
@@ -16,15 +21,49 @@ class KnowledgeBase
end
end
+ # Mass-update KB menu items
+ #
+ # @param [KnowledgeBase] knowledge_base
+ # @param [[]] params @see .update_location_params!
+ #
+ # @return []
+ def self.update_using_params!(knowledge_base, params)
+ return if params.blank?
+
+ params
+ .map { |location_params| update_location_using_params! knowledge_base, location_params }
+ .map(&:reload)
+ .reduce(:+)
+ end
+
+ # Mass-update KB menu items in a given location
+ #
+ # @param [KnowledgeBase] knowledge_base
+ # @param [Hash] location_params
+ #
+ # @option location_params [Integer] :kb_locale_id
+ # @option location_params [String] :location header or footer
+ # @option location_params [[]] :menu_items @see #update_order
+ def self.update_location_using_params!(knowledge_base, location_params)
+ action = new(
+ knowledge_base.kb_locales.find(location_params[:kb_locale_id]),
+ location_params[:location],
+ location_params[:menu_items]
+ )
+
+ action.perform!
+ action.scope
+ end
+
private
def update_order
- old_items = @kb_locale.menu_items.to_a
+ old_items = scope.to_a
@menu_items_data
.reject { |elem| elem[:_destroy] }
.each_with_index do |data_elem, index|
- item = old_items.find { |record| record.id == data_elem[:id] } || @kb_locale.menu_items.build
+ item = old_items.find { |record| record.id == data_elem[:id] } || scope.build
item.position = index
item.title = data_elem[:title]
@@ -43,7 +82,7 @@ class KnowledgeBase
end
def all_ids_present?
- old_ids = @kb_locale.menu_items.pluck(:id)
+ old_ids = scope.pluck(:id)
new_ids = @menu_items_data.map { |elem| elem[:id]&.to_i }.compact
old_ids.sort == new_ids.sort
diff --git a/spec/db/migrate/issue_2867_footer_header_public_link_spec.rb b/spec/db/migrate/issue_2867_footer_header_public_link_spec.rb
new file mode 100644
index 000000000..7898e732c
--- /dev/null
+++ b/spec/db/migrate/issue_2867_footer_header_public_link_spec.rb
@@ -0,0 +1,39 @@
+require 'rails_helper'
+
+RSpec.describe Issue2867FooterHeaderPublicLink, type: :db_migration do
+ self.use_transactional_tests = false # see comments on #without_index method
+
+ before { without_column(table, column: column) }
+
+ let(:table) { :knowledge_base_menu_items }
+ let(:column) { :location }
+
+ it 'adds an index' do
+ expect { migrate }.to change { index_exists?(table, column) }.to(true)
+ end
+
+ it 'sets no default' do
+ expect { migrate }
+ .not_to change {
+ KnowledgeBase::MenuItem.reset_column_information
+ KnowledgeBase::MenuItem.column_defaults['location']
+ }.from(nil)
+ end
+
+ it 'sets location for existing items' do
+ # create menu item without touching location column
+ menu_item = KnowledgeBase::MenuItem.acts_as_list_no_update do
+ attrs = attributes_for(:knowledge_base_menu_item)
+ attrs.delete :location
+
+ item = KnowledgeBase::MenuItem.new(attrs)
+ item.position = 0
+ item.kb_locale = create(:knowledge_base).kb_locales.first
+ item.save(validate: false)
+
+ item
+ end
+
+ expect { migrate }.to change { menu_item.reload.attributes['location'] }.from(nil).to('header')
+ end
+end
diff --git a/spec/factories/knowledge_base/menu_item.rb b/spec/factories/knowledge_base/menu_item.rb
index 182c45d84..b03d6d504 100644
--- a/spec/factories/knowledge_base/menu_item.rb
+++ b/spec/factories/knowledge_base/menu_item.rb
@@ -1,8 +1,10 @@
FactoryBot.define do
factory 'knowledge_base/menu_item', aliases: %i[knowledge_base_menu_item] do
- kb_locale { nil }
- title { Faker::Kpop.iii_groups }
- url { Faker::Internet.url }
+ kb_locale { nil }
+ sequence(:title) { |n| "menu_#{n}" }
+ url { Faker::Internet.url }
+
+ for_header
before :create do |menu_item|
if menu_item.kb_locale.blank?
@@ -10,5 +12,13 @@ FactoryBot.define do
menu_item.kb_locale = kb.kb_locales.first
end
end
+
+ trait :for_footer do
+ location { 'footer' }
+ end
+
+ trait :for_header do
+ location { 'header' }
+ end
end
end
diff --git a/spec/models/knowledge_base/menu_item_spec.rb b/spec/models/knowledge_base/menu_item_spec.rb
index b94d229dd..d8b3f47d3 100644
--- a/spec/models/knowledge_base/menu_item_spec.rb
+++ b/spec/models/knowledge_base/menu_item_spec.rb
@@ -6,39 +6,64 @@ RSpec.describe KnowledgeBase::MenuItem, type: :model do
include_context 'factory'
- context 'when url without prefix is added' do
- before { kb_menu_item.update(url: Faker::Internet.domain_name) }
+ context 'item' do
+ it { is_expected.to validate_presence_of :title }
+ it { is_expected.to validate_presence_of :url }
+ it { is_expected.to validate_presence_of :location }
+ it { is_expected.to validate_inclusion_of(:location).in_array(%w[header footer]) }
+ end
- it 'is saved' do
- expect(kb_menu_item).not_to be_changed
+ context 'has scopes for' do
+ let(:kb_locale) { kb_menu_item.kb_locale }
+ let(:scope) { described_class.where(kb_locale: kb_locale) }
+
+ let!(:header) { create(:knowledge_base_menu_item, :for_header, kb_locale: kb_locale) }
+ let!(:footer) { create(:knowledge_base_menu_item, :for_footer, kb_locale: kb_locale) }
+
+ it 'header' do
+ expect(scope.location_header).to match [kb_menu_item, header]
end
- it 'prefix is added to hostname' do
- expect(kb_menu_item.url).to start_with 'http://'
+ it 'footer' do
+ expect(scope.location_footer).to match [footer]
end
end
- context 'when url with custom prefix is added' do
- before { kb_menu_item.update(url: "scheme://#{Faker::Internet.domain_name}") }
+ context 'when url' do
+ context 'without prefix is added' do
+ before { kb_menu_item.update(url: Faker::Internet.domain_name) }
- it 'is saved' do
- expect(kb_menu_item).not_to be_changed
+ it 'is saved' do
+ expect(kb_menu_item).not_to be_changed
+ end
+
+ it 'prefix is added to hostname' do
+ expect(kb_menu_item.url).to start_with 'http://'
+ end
end
- it 'given scheme is not touched' do
- expect(kb_menu_item.url).to start_with 'scheme://'
- end
- end
+ context 'with custom prefix is added' do
+ before { kb_menu_item.update(url: "scheme://#{Faker::Internet.domain_name}") }
- context 'protocol prefix is not added to relative url' do
- before { kb_menu_item.update(url: '/loremipsum') }
+ it 'is saved' do
+ expect(kb_menu_item).not_to be_changed
+ end
- it 'is saved' do
- expect(kb_menu_item).not_to be_changed
+ it 'given scheme is not touched' do
+ expect(kb_menu_item.url).to start_with 'scheme://'
+ end
end
- it 'path is not modified' do
- expect(kb_menu_item.url).not_to start_with 'http://'
+ context 'is relative and protocol prefix is not added' do
+ before { kb_menu_item.update(url: '/loremipsum') }
+
+ it 'is saved' do
+ expect(kb_menu_item).not_to be_changed
+ end
+
+ it 'path is not modified' do
+ expect(kb_menu_item.url).not_to start_with 'http://'
+ end
end
end
end
diff --git a/spec/requests/admin/knowledge_base/public_menu_spec.rb b/spec/requests/admin/knowledge_base/public_menu_spec.rb
new file mode 100644
index 000000000..4c89634d2
--- /dev/null
+++ b/spec/requests/admin/knowledge_base/public_menu_spec.rb
@@ -0,0 +1,67 @@
+require 'rails_helper'
+
+RSpec.describe 'Admin Knowledge Base Public Menu', type: :request, authenticated_as: :admin_user do
+ let(:url) { "/api/v1/knowledge_bases/manage/#{knowledge_base.id}/update_menu_items" }
+ let(:params) do
+ {
+ menu_items_sets: [{
+ "kb_locale_id": kb_locale.id,
+ "location": location,
+ "menu_items": menu_items
+ }]
+ }
+ end
+
+ let(:menu_item) { create(:knowledge_base_menu_item) }
+ let(:kb_locale) { menu_item.kb_locale }
+ let(:knowledge_base) { kb_locale.knowledge_base }
+ let(:location) { 'header' }
+
+ it 'edit title' do
+ attrs = to_params(menu_item)
+ attrs[:title] = 'new title'
+
+ params = build_params([attrs])
+
+ expect { make_request(params) }.to change { menu_item.reload.title }.to 'new title'
+ end
+
+ it 'delete item' do
+ attrs = to_params(menu_item)
+ attrs[:_destroy] = true
+
+ params = build_params([attrs])
+
+ expect { make_request(params) }.to change { KnowledgeBase::MenuItem.count }.by(-1)
+ end
+
+ it 'add item' do
+ new_item = {
+ title: 'new item',
+ new_tab: false,
+ url: '/new_url'
+ }
+
+ params = build_params([to_params(menu_item), new_item])
+
+ expect { make_request(params) }.to change { KnowledgeBase::MenuItem.count }.by(1)
+ end
+
+ def to_params(item)
+ item.slice :id, :title, :url, :new_tab
+ end
+
+ def make_request(params)
+ patch url, params: params, as: :json
+ end
+
+ def build_params(menu_items)
+ {
+ menu_items_sets: [{
+ "kb_locale_id": kb_locale.id,
+ "location": location,
+ "menu_items": menu_items
+ }]
+ }
+ end
+end
diff --git a/spec/support/db_migration.rb b/spec/support/db_migration.rb
index 7a4cfcb03..beb8a407f 100644
--- a/spec/support/db_migration.rb
+++ b/spec/support/db_migration.rb
@@ -53,6 +53,27 @@ module DbMigrationHelper
end
end
+ # Helper method for setting up specs on DB migrations that add columns.
+ # Make sure to define type: :db_migration in your RSpec.describe call
+ # and add `self.use_transactional_tests = false` to your context.
+ #
+ # @param [Symbol] from_table the name of the table with the indexed column
+ # @param [Symbol] name(s) of indexed column(s)
+ #
+ # @example
+ # without_column(:online_notifications, column: :user_id)
+ #
+ # @return [nil]
+ def without_column(from_table, column:)
+ suppress_messages do
+ Array(column).each do |elem|
+ next unless column_exists?(from_table, elem)
+
+ remove_column(from_table, elem)
+ end
+ end
+ end
+
# Helper method for setting up specs on DB migrations that add indices.
# Make sure to define type: :db_migration in your RSpec.describe call
# and add `self.use_transactional_tests = false` to your context.
diff --git a/spec/support/knowledge_base_contexts.rb b/spec/support/knowledge_base_contexts.rb
index 780106da2..b3ae2bbde 100644
--- a/spec/support/knowledge_base_contexts.rb
+++ b/spec/support/knowledge_base_contexts.rb
@@ -35,3 +35,11 @@ RSpec.shared_context 'basic Knowledge Base', current_user_id: 1 do
create(:knowledge_base_answer, category: category, archived_at: 1.week.ago)
end
end
+
+RSpec.shared_context 'Knowledge Base menu items', current_user_id: 1 do
+ let!(:menu_item_1) { create(:knowledge_base_menu_item, :for_header, kb_locale: primary_locale) }
+ let!(:menu_item_2) { create(:knowledge_base_menu_item, :for_header, kb_locale: primary_locale) }
+ let!(:menu_item_3) { create(:knowledge_base_menu_item, :for_footer, kb_locale: primary_locale) }
+ let!(:menu_item_4) { create(:knowledge_base_menu_item, :for_footer, kb_locale: alternative_locale) }
+ let!(:menu_item_5) { create(:knowledge_base_menu_item, :for_footer, kb_locale: alternative_locale) }
+end
diff --git a/spec/system/admin/knowledge_base/public_menu_spec.rb b/spec/system/admin/knowledge_base/public_menu_spec.rb
new file mode 100644
index 000000000..61d9e13bb
--- /dev/null
+++ b/spec/system/admin/knowledge_base/public_menu_spec.rb
@@ -0,0 +1,74 @@
+require 'rails_helper'
+
+# https://github.com/zammad/zammad/issues/266
+RSpec.describe 'Admin Panel > Knowledge Base > Public Menu', type: :system, authenticated: true do
+ include_context 'basic Knowledge Base'
+ include_context 'Knowledge Base menu items'
+
+ before do
+ visit '/#manage/knowledge_base'
+ find('a', text: 'Public Menu').click
+ end
+
+ context 'lists menu items' do
+ it { expect(find_locale('Footer menu', alternative_locale).text).to include menu_item_4.title }
+ it { expect(find_locale('Header menu', primary_locale).text).to include menu_item_1.title }
+ it { expect(find_locale('Header menu', alternative_locale).text).not_to include menu_item_2.title }
+ it { expect(find_locale('Header menu', primary_locale).text).to include menu_item_2.title }
+ end
+
+ context 'edit menu items' do
+ before do
+ find_location('Header menu').find('a', text: 'Edit').click
+
+ modal_ready
+ end
+
+ it 'edit menu item' do
+ find('input') { |elem| elem.value == menu_item_1.title }.fill_in with: 'test menu'
+ find('button', text: 'Submit').click
+
+ modal_disappear
+
+ expect(find_locale('Header menu', primary_locale).text).to include 'test menu'
+ end
+
+ it 'adds menu item' do
+ container = find(:css, '.modal-body h2', text: alternative_locale.system_locale.name).find(:xpath, '..')
+ container.find('a', text: 'Add').click
+
+ container.find('input') { |elem| elem['data-name'] == 'title' }.fill_in with: 'new item'
+ container.find('input') { |elem| elem['data-name'] == 'url' }.fill_in with: '/new_item'
+
+ find('button', text: 'Submit').click
+
+ modal_disappear
+
+ expect(find_locale('Header menu', alternative_locale).text).to include 'new item'
+ end
+
+ it 'deletes menu item' do
+ find(:css, '.modal-body')
+ .find('input') { |elem| elem.value == menu_item_1.title }
+ .ancestor('tr')
+ .find('.js-remove')
+ .click
+
+ find('button', text: 'Submit').click
+
+ modal_disappear
+
+ expect(find_locale('Header menu', alternative_locale).text).not_to include menu_item_1.title
+ end
+ end
+
+ def find_locale(location, locale)
+ find_location(location)
+ .find('.label', text: /#{Regexp.escape locale.system_locale.name}/i)
+ .ancestor('.kb-menu-preview')
+ end
+
+ def find_location(location)
+ find('h3', text: location).ancestor('.settings-entry')
+ end
+end
diff --git a/spec/system/knowledge_base_public/menu_items_spec.rb b/spec/system/knowledge_base_public/menu_items_spec.rb
new file mode 100644
index 000000000..e291e2190
--- /dev/null
+++ b/spec/system/knowledge_base_public/menu_items_spec.rb
@@ -0,0 +1,38 @@
+require 'rails_helper'
+
+RSpec.describe 'Public Knowledge Base menu items', type: :system, authenticated: false do
+ include_context 'basic Knowledge Base'
+ include_context 'Knowledge Base menu items'
+
+ before do
+ published_answer
+
+ visit help_no_locale_path
+ end
+
+ it 'shows header public link' do
+ expect(page).to have_css('header .menu-item', text: menu_item_1.title)
+ end
+
+ it 'shows another header public link' do
+ expect(page).to have_css('header .menu-item', text: menu_item_2.title)
+ end
+
+ it "doesn't show footer link in header" do
+ expect(page).not_to have_css('header .menu-item', text: menu_item_3.title)
+ end
+
+ it 'shows footer public link' do
+ expect(page).to have_css('footer .menu-item', text: menu_item_3.title)
+ end
+
+ it "doesn't show footer link of another locale" do
+ expect(page).not_to have_css('footer .menu-item', text: menu_item_4.title)
+ end
+
+ it 'shows public links in given order' do
+ index_1 = page.body.index menu_item_1.title
+ index_2 = page.body.index menu_item_2.title
+ expect(index_1).to be < index_2
+ end
+end