diff --git a/app/assets/javascripts/alchemy/alchemy.base.js.coffee b/app/assets/javascripts/alchemy/alchemy.base.js.coffee
index 6458d2de0c..def8c9c364 100644
--- a/app/assets/javascripts/alchemy/alchemy.base.js.coffee
+++ b/app/assets/javascripts/alchemy/alchemy.base.js.coffee
@@ -68,7 +68,7 @@ $.extend Alchemy,
Alchemy.setElementDirty $element
false
- # Initializes all select tag with .alchemy_selectbox class as selectBoxIt instance
+ # Initializes all select tag with .alchemy_selectbox class as select2 instance
# Pass a jQuery scope to only init a subset of selectboxes.
SelectBox: (scope) ->
$("select.alchemy_selectbox", scope).select2
diff --git a/app/assets/stylesheets/alchemy/admin.scss b/app/assets/stylesheets/alchemy/admin.scss
index e63b96f752..013e889b4d 100644
--- a/app/assets/stylesheets/alchemy/admin.scss
+++ b/app/assets/stylesheets/alchemy/admin.scss
@@ -30,6 +30,7 @@
@import "alchemy/icons";
@import "alchemy/image_library";
@import "alchemy/labels";
+@import "alchemy/nodes";
@import "alchemy/notices";
@import "alchemy/pagination";
@import "alchemy/preview_window";
diff --git a/app/assets/stylesheets/alchemy/forms.scss b/app/assets/stylesheets/alchemy/forms.scss
index 9970a0ab44..c07e9119c7 100644
--- a/app/assets/stylesheets/alchemy/forms.scss
+++ b/app/assets/stylesheets/alchemy/forms.scss
@@ -38,6 +38,10 @@ form {
float: right;
}
+ .input > .select2-container {
+ width: 100%;
+ }
+
> .autocomplete_tag_list {
.select2-container, .select2-choices {
@@ -49,11 +53,8 @@ form {
line-height: 16px;
}
- &.select, &.grouped_select {
-
- .select2-container {
- margin: 4px 0;
- }
+ .select2-container {
+ margin: 4px 0;
}
&.boolean {
diff --git a/app/assets/stylesheets/alchemy/nodes.scss b/app/assets/stylesheets/alchemy/nodes.scss
new file mode 100644
index 0000000000..6e2d4790e8
--- /dev/null
+++ b/app/assets/stylesheets/alchemy/nodes.scss
@@ -0,0 +1,154 @@
+.nodes_tree.list {
+ margin: 2em 0;
+
+ &.sorting {
+ padding-top: 100px;
+
+ .page_icon {
+ cursor: move
+ }
+ }
+
+ .sitemap_node-level_0 {
+
+ > .node_name {
+ font-weight: bold;
+ }
+ }
+
+ .node_page,
+ .node_url {
+ width: 200px;
+ max-width: 45%;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+
+ > a {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 100%;
+
+ .external & {
+ max-width: 90%;
+ }
+ }
+ }
+
+ .node_page {
+ padding: 0 8px;
+ margin-left: auto;
+ }
+
+ .node_url {
+ display: flex;
+ align-items: center;
+ padding: 0 2*$default-padding;
+ white-space: nowrap;
+ background-color: $sitemap-info-background-color;
+ line-height: $sitemap-line-height;
+ font-size: $small-font-size;
+ @include border-right-radius($default-border-radius);
+
+ > i {
+ margin-left: auto;
+ padding-left: $default-padding;
+ }
+ }
+
+ .node_folder {
+ cursor: pointer;
+ }
+
+ ul {
+ margin: 0;
+ padding: 0;
+ }
+
+ li {
+ line-height: $sitemap-line-height;
+ padding-left: $default-padding;
+
+ li {
+ padding-left: $sitemap-line-height;
+ }
+ }
+}
+
+#node_filter_result {
+ display: none;
+ margin-left: 2*$default-margin;
+}
+
+.sitemap_node {
+ margin: 3*$default-margin 0;
+ transition: background-color $transition-duration;
+
+ &.highlight {
+ background-color: $sitemap-highlight-color;
+ }
+
+ &.no-match .sitemap_pagename_link {
+ color: $medium-gray;
+ }
+
+ &:hover {
+ background-color: $sitemap-page-hover-color;
+ border-radius: $default-border-radius;
+ }
+
+ .node_name {
+ display: flex;
+ justify-content: space-between;
+ @include border-left-radius($default-border-radius);
+ padding: 0 0 0 10px;
+ margin: 2px;
+ text-decoration: none;
+ overflow: hidden;
+ background-color: $sitemap-page-background-color;
+
+ &.without-status {
+ @include border-right-radius($default-border-radius);
+ }
+
+ &.inactive {
+ color: #656565;
+ }
+ }
+}
+
+.nodes_tree-left_images {
+ position: relative;
+ width: 32px;
+ line-height: $sitemap-line-height;
+ float: left;
+ padding: 0 2*$default-padding;
+ text-align: center;
+}
+
+.nodes_tree-right_tools {
+ height: $sitemap-line-height;
+ padding: 0 2*$default-padding;
+ float: right;
+
+ > a {
+ float: left;
+ width: $sitemap-line-height;
+ height: $sitemap-line-height;
+ line-height: $sitemap-line-height;
+ text-align: center;
+ margin: 0;
+
+ &.disabled .icon {
+ opacity: 0.25;
+ filter: grayscale(100%);
+ }
+ }
+
+ .icon.blank {
+ margin-left: 2px;
+ float: left;
+ margin-top: 3px;
+ margin-right: 3px;
+ }
+}
diff --git a/app/assets/stylesheets/alchemy/selects.scss b/app/assets/stylesheets/alchemy/selects.scss
index 326403a8db..8a2cf97bb6 100644
--- a/app/assets/stylesheets/alchemy/selects.scss
+++ b/app/assets/stylesheets/alchemy/selects.scss
@@ -40,6 +40,10 @@ select {
font-weight: normal;
text-align: left;
+ .select2-chosen {
+ overflow: visible;
+ }
+
.select2-arrow {
top: 0;
width: $form-field-height;
diff --git a/app/controllers/alchemy/admin/nodes_controller.rb b/app/controllers/alchemy/admin/nodes_controller.rb
new file mode 100644
index 0000000000..6de40115a3
--- /dev/null
+++ b/app/controllers/alchemy/admin/nodes_controller.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Alchemy
+ module Admin
+ class NodesController < Admin::ResourcesController
+ def index
+ @root_nodes = Node.language_root_nodes
+ end
+
+ def new
+ @node = Node.new(
+ parent_id: params[:parent_id],
+ language: Language.current
+ )
+ end
+
+ def toggle
+ node = Node.find(params[:id])
+ node.update(folded: !node.folded)
+ if node.folded?
+ head :ok
+ else
+ render partial: 'node', collection: node.children.includes(:page, :children)
+ end
+ end
+
+ private
+
+ def resource_params
+ params.require(:node).permit(
+ :parent_id,
+ :language_id,
+ :page_id,
+ :name,
+ :url,
+ :title,
+ :nofollow,
+ :external
+ )
+ end
+ end
+ end
+end
diff --git a/app/helpers/alchemy/pages_helper.rb b/app/helpers/alchemy/pages_helper.rb
index 1cd4197caa..7b0f00d886 100644
--- a/app/helpers/alchemy/pages_helper.rb
+++ b/app/helpers/alchemy/pages_helper.rb
@@ -73,6 +73,7 @@ def render_site_layout
end
# Renders the navigation.
+ # @deprecated
#
# It produces a html
+ <%= render 'alchemy/admin/partials/site_select' %>
+ <%= render 'alchemy/admin/partials/language_tree_select' %>
+ <%= toolbar_button(
+ icon: 'plus',
+ label: Alchemy.t(:create_menu),
+ url: alchemy.new_admin_node_path,
+ hotkey: 'alt+n',
+ dialog_options: {
+ title: Alchemy.t(:create_menu),
+ size: '450x120'
+ },
+ if_permitted_to: [:create, Alchemy::Node]
+ ) %>
+
+<% end %>
+
+
<%= render 'alchemy/admin/pages/publication_fields' %>
- <%= page_status_checkbox(@page, :visible) %>
<%= page_status_checkbox(@page, :restricted) %>
+ <%= render 'alchemy/admin/pages/menu_fields', f: f %>
<% if configuration(:sitemap)['show_flag'] %>
<%= page_status_checkbox(@page, :sitemap) %>
<% end %>
diff --git a/app/views/alchemy/admin/pages/_menu_fields.html.erb b/app/views/alchemy/admin/pages/_menu_fields.html.erb
new file mode 100644
index 0000000000..dadb0f2641
--- /dev/null
+++ b/app/views/alchemy/admin/pages/_menu_fields.html.erb
@@ -0,0 +1,33 @@
+<% if @page.menus.any? %>
+
+ <% @page.menus.each do |menu| %>
+
+ <% end %>
+<% elsif Alchemy::Node.roots.any? %>
+ <%= page_status_checkbox(@page, :visible) %>
+ <%= f.input :menu_id, collection: Alchemy::Node.roots.map { |n| [n.name, n.id] },
+ prompt: Alchemy.t('Please choose a menu'),
+ input_html: { class: 'alchemy_selectbox' },
+ wrapper_html: { style: @page.visible? ? 'display: block' : 'display: none' },
+ label: false %>
+
+<% else %>
+ <%= page_status_checkbox(@page, :visible) %>
+<% end %>
diff --git a/config/alchemy/modules.yml b/config/alchemy/modules.yml
index d48c3eba04..dc8414a3b4 100644
--- a/config/alchemy/modules.yml
+++ b/config/alchemy/modules.yml
@@ -28,9 +28,18 @@
- controller: 'alchemy/admin/pages'
action: edit
-- name: languages
+- name: menus
engine_name: alchemy
position: 3
+ navigation:
+ name: 'modules.menus'
+ controller: 'alchemy/admin/nodes'
+ action: index
+ icon: list-ul
+
+- name: languages
+ engine_name: alchemy
+ position: 4
navigation:
name: 'modules.languages'
controller: 'alchemy/admin/languages'
@@ -39,7 +48,7 @@
- name: sites
engine_name: alchemy
- position: 4
+ position: 5
navigation:
name: 'modules.sites'
controller: 'alchemy/admin/sites'
@@ -48,7 +57,7 @@
- name: tags
engine_name: alchemy
- position: 5
+ position: 6
navigation:
name: 'modules.tags'
controller: 'alchemy/admin/tags'
@@ -57,7 +66,7 @@
- name: archive
engine_name: alchemy
- position: 6
+ position: 7
navigation:
controller: 'alchemy/admin/pictures'
action: index
diff --git a/config/locales/alchemy.en.yml b/config/locales/alchemy.en.yml
index 9582c3e451..d7593224c1 100644
--- a/config/locales/alchemy.en.yml
+++ b/config/locales/alchemy.en.yml
@@ -305,6 +305,7 @@ en:
assign_file: "Assign a file"
assign_file_from_archive: "assign a file from your archive"
assign_image: "Assign an image"
+ attached_to: "attached to"
attachment_filename_notice: "* Please do not use any special characters for the filename."
auto_play: "Play movie after load"
big_thumbnails: "Big thumbnails"
@@ -320,6 +321,8 @@ en:
confirm_to_delete_image: "Do you really want to delete this image from server?"
confirm_to_delete_image_from_server: "Do you really want to delete this image from the server?"
confirm_to_delete_images_from_server: "Do you really want to delete these images from the server?"
+ confirm_to_delete_menu: "Do you really want to delete this menu?"
+ confirm_to_delete_node: "Do you really want to delete this menu node?"
confirm_to_delete_page: "Do you really want to delete this page? All its elements (even trashed ones) will get lost!"
content_essence_not_found: "Content essence not found"
content_not_found: "Field for content not present."
@@ -335,12 +338,16 @@ en:
"Create language": "Create a new language"
"Create site": "Create a new site"
create_language_tree_heading: "Create empty language tree"
+ create_menu: "Add a menu"
+ create_node: "Add a menu node"
create_page: "Create a new subpage"
currently_edited_by: "This page is locked by"
cut_element: "Cut this element."
delete_file: "Delete this file from server."
delete_image: "Remove this image"
delete_language: "Delete this language"
+ delete_menu: "Delete this menu"
+ delete_node: "Delete this menu node"
delete_page: "Delete this page"
delete_tag: 'Delete tag'
document: "File"
@@ -351,6 +358,8 @@ en:
edit_file_properties: "Edit file properties."
edit_image_properties: "Edit image properties."
edit_language: "Edit language"
+ edit_menu: "Edit menu"
+ edit_node: "Edit menu node"
edit_page: "Edit this page"
edit_page_properties: "Edit page properties"
edit_tag: 'Edit tag'
@@ -418,6 +427,7 @@ en:
male: "Male"
me: "Me"
medium_thumbnails: "Medium thumbnails"
+ menu: Menu
meta_data: "Meta-Data"
meta_description: "Meta-Description"
meta_keywords: "Meta-Keywords"
@@ -428,6 +438,7 @@ en:
languages: "Languages"
layoutpages: "Global Pages"
library: "Library"
+ menus: "Menus"
pages: "Pages"
tags: "Tags"
sites: "Sites"
@@ -435,7 +446,7 @@ en:
users: "Users"
name: "Name"
names: "Names"
- navigation_name: "Navigation name"
+ node_url_hint: "Please use either a leading slash (/) or an url with protocol (ie. https:)"
no_image_for_cropper_found: "No image found. Please save the element first."
no: "No"
"no pages": "no pages"
@@ -445,6 +456,7 @@ en:
no_files_in_archive: "You do not have any files in your archive."
no_images_in_archive: "You don't have any images in your archive."
no_more_elements_to_add: "No more elements available."
+ no_resource_found: "No %{resource} found. Please add your first one below."
no_search_results: "Your search did not return any results."
"not a valid image": "This is not an valid image."
"or": 'or'
@@ -704,6 +716,9 @@ en:
alchemy/language:
one: "Language"
other: "Languages"
+ alchemy/node:
+ one: "Menu node"
+ other: "Menu nodes"
alchemy/page:
one: "Page"
other: "Pages"
@@ -762,6 +777,13 @@ en:
code: ISO Code
alchemy/legacy_page_url:
urlname: "URL path"
+ alchemy/node:
+ name: "Name"
+ title: "Title"
+ nofollow: "Search engine must not follow"
+ url: "URL"
+ page: "Page"
+ external: "Open link in new tab"
alchemy/page:
created_at: "Created at"
language: "Language"
diff --git a/config/routes.rb b/config/routes.rb
index b4a1479228..2353d5b4c1 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -17,6 +17,12 @@
namespace :admin, {path: Alchemy.admin_path, constraints: Alchemy.admin_constraints} do
resources :contents, only: [:create]
+ resources :nodes do
+ member do
+ patch :toggle
+ end
+ end
+
resources :pages do
resources :elements
collection do
diff --git a/db/migrate/20191029212236_create_alchemy_nodes.rb b/db/migrate/20191029212236_create_alchemy_nodes.rb
new file mode 100644
index 0000000000..70ba5332ee
--- /dev/null
+++ b/db/migrate/20191029212236_create_alchemy_nodes.rb
@@ -0,0 +1,24 @@
+class CreateAlchemyNodes < ActiveRecord::Migration[5.0]
+ def change
+ create_table :alchemy_nodes do |t|
+ t.string :name
+ t.string :title
+ t.string :url
+ t.boolean :nofollow, null: false, default: false
+ t.boolean :external, null: false, default: false
+ t.boolean :folded, null: false, default: false
+
+ t.integer :parent_id, index: true
+ t.integer :lft, null: false, index: true
+ t.integer :rgt, null: false, index: true
+ t.integer :depth, null: false, default: 0
+
+ t.references :page, foreign_key: { to_table: :alchemy_pages, on_delete: :cascade }
+ t.references :language, null: false, foreign_key: { to_table: :alchemy_languages }
+ t.references :creator, index: true
+ t.references :updater, index: true
+
+ t.timestamps
+ end
+ end
+end
diff --git a/lib/alchemy/permissions.rb b/lib/alchemy/permissions.rb
index 0e86878355..951a6c118a 100644
--- a/lib/alchemy/permissions.rb
+++ b/lib/alchemy/permissions.rb
@@ -95,6 +95,7 @@ def alchemy_author_rules
:alchemy_admin_attachments,
:alchemy_admin_dashboard,
:alchemy_admin_layoutpages,
+ :alchemy_admin_nodes,
:alchemy_admin_pages,
:alchemy_admin_pictures,
:alchemy_admin_tags,
@@ -116,6 +117,7 @@ def alchemy_author_rules
can :manage, Alchemy::EssenceFile
can :manage, Alchemy::EssencePicture
can :manage, Alchemy::LegacyPageUrl
+ can :manage, Alchemy::Node
can :read, Alchemy::Picture
can [:read, :autocomplete], Alchemy::Tag
can(:edit_content, Alchemy::Page) { |p| p.editable_by?(@user) }
diff --git a/lib/alchemy/test_support/factories/node_factory.rb b/lib/alchemy/test_support/factories/node_factory.rb
new file mode 100644
index 0000000000..e031e43688
--- /dev/null
+++ b/lib/alchemy/test_support/factories/node_factory.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'factory_bot'
+require 'alchemy/test_support/factories/language_factory'
+require 'alchemy/test_support/factories/page_factory'
+
+FactoryBot.define do
+ factory :alchemy_node, class: 'Alchemy::Node' do
+ language { Alchemy::Language.default }
+
+ trait :with_name do
+ name { 'A Node' }
+ end
+
+ trait :with_page do
+ association :page, factory: :alchemy_page
+ end
+
+ trait :with_url do
+ url { 'https://example.com' }
+ end
+ end
+end
diff --git a/lib/rails/generators/alchemy/menus/menus_generator.rb b/lib/rails/generators/alchemy/menus/menus_generator.rb
new file mode 100644
index 0000000000..40b4d91f65
--- /dev/null
+++ b/lib/rails/generators/alchemy/menus/menus_generator.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require_relative '../base'
+
+module Alchemy
+ module Generators
+ class MenusGenerator < Base
+ desc "This generator generates Alchemy menu partials."
+ source_root File.expand_path('templates', __dir__)
+
+ def create_partials
+ menus = Alchemy::Node.roots
+ return unless menus
+
+ menus.each do |menu|
+ conditional_template "wrapper.html.#{template_engine}",
+ "app/views/#{menu.view_folder_name}/_wrapper.html.#{template_engine}"
+ conditional_template "node.html.#{template_engine}",
+ "app/views/#{menu.view_folder_name}/_node.html.#{template_engine}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/rails/generators/alchemy/menus/templates/node.html.erb b/lib/rails/generators/alchemy/menus/templates/node.html.erb
new file mode 100644
index 0000000000..f3dce4c726
--- /dev/null
+++ b/lib/rails/generators/alchemy/menus/templates/node.html.erb
@@ -0,0 +1,17 @@
+<%%= content_tag :li, class: ['nav-item', node.children.any? ? 'dropdown' : nil].compact do %>
+ <%%= link_to_if node.url,
+ node.name,
+ @preview_mode ? 'javascript: void(0)' : node.url,
+ class: ['nav-link', current_page?(node.url) ? 'active' : nil].compact,
+ title: node.title,
+ target: node.external? ? '_blank' : nil,
+ rel: node.nofollow? ? 'nofollow' : nil %>
+ <%% if node.children.any? %>
+
+ <%% end %>
+<%% end %>
diff --git a/lib/rails/generators/alchemy/menus/templates/node.html.haml b/lib/rails/generators/alchemy/menus/templates/node.html.haml
new file mode 100644
index 0000000000..058d6f74d2
--- /dev/null
+++ b/lib/rails/generators/alchemy/menus/templates/node.html.haml
@@ -0,0 +1,15 @@
+= content_tag :li,
+ class: ['nav-item', node.children.any? ? 'dropdown' : nil].compact do
+ = link_to_if node.url,
+ node.name,
+ @preview_mode ? 'javascript: void(0)' : node.url,
+ class: ['nav-link', current_page?(node.url) ? 'active' : nil].compact,
+ title: node.title,
+ target: node.external? ? '_blank' : nil,
+ rel: node.nofollow? ? 'nofollow' : nil
+ - if node.children.any?
+ %ul.dropdown-menu
+ = render partial: options[:node_partial_name],
+ collection: node.children.includes(:page, :children),
+ locals: { options: options },
+ as: 'node'
diff --git a/lib/rails/generators/alchemy/menus/templates/node.html.slim b/lib/rails/generators/alchemy/menus/templates/node.html.slim
new file mode 100644
index 0000000000..aa88ff6a0e
--- /dev/null
+++ b/lib/rails/generators/alchemy/menus/templates/node.html.slim
@@ -0,0 +1,15 @@
+= content_tag :li,
+ class: ['nav-item', node.children.any? ? 'dropdown' : nil].compact do
+ = link_to_if node.url,
+ node.name,
+ @preview_mode ? 'javascript: void(0)' : node.url,
+ class: ['nav-link', current_page?(node.url) ? 'active' : nil].compact,
+ title: node.title,
+ target: node.external? ? '_blank' : nil,
+ rel: node.nofollow? ? 'nofollow' : nil
+ - if node.children.any?
+ ul.dropdown-menu
+ = render partial: options[:node_partial_name],
+ collection: node.children.includes(:page, :children),
+ locals: { options: options },
+ as: 'node'
diff --git a/lib/rails/generators/alchemy/menus/templates/wrapper.html.erb b/lib/rails/generators/alchemy/menus/templates/wrapper.html.erb
new file mode 100644
index 0000000000..d648063f73
--- /dev/null
+++ b/lib/rails/generators/alchemy/menus/templates/wrapper.html.erb
@@ -0,0 +1,6 @@
+
+ <%%= render partial: options[:node_partial_name],
+ collection: node.children.includes(:page, :children),
+ locals: { options: options },
+ as: 'node' %>
+
diff --git a/lib/rails/generators/alchemy/menus/templates/wrapper.html.haml b/lib/rails/generators/alchemy/menus/templates/wrapper.html.haml
new file mode 100644
index 0000000000..8021b59b6b
--- /dev/null
+++ b/lib/rails/generators/alchemy/menus/templates/wrapper.html.haml
@@ -0,0 +1,5 @@
+%ul.nav
+ = render partial: options[:node_partial_name],
+ collection: node.children.includes(:page, :children),
+ locals: { options: options },
+ as: 'node' %>
diff --git a/lib/rails/generators/alchemy/menus/templates/wrapper.html.slim b/lib/rails/generators/alchemy/menus/templates/wrapper.html.slim
new file mode 100644
index 0000000000..4afb2ece57
--- /dev/null
+++ b/lib/rails/generators/alchemy/menus/templates/wrapper.html.slim
@@ -0,0 +1,5 @@
+ul.nav
+ = render partial: options[:node_partial_name],
+ collection: node.children.includes(:page, :children),
+ locals: { options: options },
+ as: 'node' %>
diff --git a/lib/rails/generators/alchemy/views/views_generator.rb b/lib/rails/generators/alchemy/views/views_generator.rb
index d193ceffcf..93e263781b 100644
--- a/lib/rails/generators/alchemy/views/views_generator.rb
+++ b/lib/rails/generators/alchemy/views/views_generator.rb
@@ -3,7 +3,7 @@
module Alchemy
module Generators
class ViewsGenerator < ::Rails::Generators::Base
- ALCHEMY_VIEWS = %w(breadcrumb language_links messages_mailer navigation)
+ ALCHEMY_VIEWS = %w(breadcrumb language_links messages_mailer)
desc "Generates Alchemy views for #{ALCHEMY_VIEWS.to_sentence}."
diff --git a/lib/tasks/alchemy/convert.rake b/lib/tasks/alchemy/convert.rake
index be1ded824b..8b42d1fdb9 100644
--- a/lib/tasks/alchemy/convert.rake
+++ b/lib/tasks/alchemy/convert.rake
@@ -31,5 +31,49 @@ namespace :alchemy do
puts "Done."
end
end
+
+ namespace :page_trees do
+ desc "Converts the page tree into a menu."
+ task to_menus: [:environment] do
+ if Alchemy::Node.roots.exists?
+ abort "\n⨯ There are already menus present in your database. Aborting!"
+ end
+
+ def convert_to_nodes(children, node:)
+ children.each do |page|
+ has_children = page.children.any?
+ next unless page.visible || has_children
+
+ Alchemy::Deprecation.silence do
+ new_node = node.children.create!(
+ name: page.visible? && page.public? && !page.redirects_to_external? ? nil : page.name,
+ page: page.visible? && page.public? && !page.redirects_to_external? ? page : nil,
+ url: page.redirects_to_external? ? page.urlname : nil,
+ external: page.redirects_to_external? && Alchemy::Config.get(:open_external_links_in_new_tab),
+ language_id: page.language_id
+ )
+ print "."
+ if has_children
+ convert_to_nodes(page.children, node: new_node)
+ end
+ end
+ end
+ end
+
+ menu_count = Alchemy::Language.count
+ puts "\n- Converting #{menu_count} page #{'tree'.pluralize(menu_count)} into #{'menu'.pluralize(menu_count)}."
+ Alchemy::BaseRecord.transaction do
+ Alchemy::Language.all.each do |language|
+ locale = language.locale.presence || I18n.default_locale
+ menu_name = I18n.t('Main Navigation', scope: 'alchemy.menu_names', default: 'Main Navigation', locale: locale)
+ root_node = Alchemy::Node.create(language: language, name: menu_name)
+ language.pages.language_roots.each do |root_page|
+ convert_to_nodes(root_page.children, node: root_node)
+ end
+ end
+ end
+ puts "\n✓ Done."
+ end
+ end
end
end
diff --git a/spec/controllers/alchemy/admin/nodes_controller_spec.rb b/spec/controllers/alchemy/admin/nodes_controller_spec.rb
new file mode 100644
index 0000000000..83bf187953
--- /dev/null
+++ b/spec/controllers/alchemy/admin/nodes_controller_spec.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+module Alchemy
+ describe Admin::NodesController do
+ routes { Alchemy::Engine.routes }
+
+ before do
+ authorize_user(:as_admin)
+ end
+
+ describe '#index' do
+ context 'if root nodes present' do
+ let!(:root_node) { create(:alchemy_node) }
+ let!(:child_node) { create(:alchemy_node, parent_id: root_node.id) }
+
+ it "loads only root nodes from current language" do
+ get :index
+ expect(assigns('root_nodes').to_a).to eq([root_node])
+ expect(assigns('root_nodes').to_a).to_not eq([child_node])
+ end
+ end
+ end
+
+ describe '#new' do
+ it "sets the current language on new node" do
+ get :new
+ expect(assigns('node').language).to eq(Language.current)
+ end
+
+ context 'with parent id in params' do
+ it "sets it to new node" do
+ get :new, params: { parent_id: 1 }
+ expect(assigns('node').parent_id).to eq(1)
+ end
+ end
+ end
+
+ describe '#create' do
+ context 'with valid params' do
+ let(:language) { create(:alchemy_language) }
+
+ it "creates node and redirects to index" do
+ expect {
+ post :create, params: { node: { name: 'Node', language_id: language.id } }
+ }.to change { Alchemy::Node.count }.by(1)
+ expect(response).to redirect_to(admin_nodes_path)
+ end
+ end
+ end
+
+ describe '#update' do
+ let(:node) { create(:alchemy_node) }
+
+ context 'with valid params' do
+ it "redirects to nodes path" do
+ put :update, params: { id: node.id, node: { name: 'Node'} }
+ expect(response).to redirect_to(admin_nodes_path)
+ end
+ end
+ end
+
+ describe '#toggle' do
+ context 'with expanded node' do
+ let(:node) { create(:alchemy_node, folded: false) }
+
+ it "folds node" do
+ expect {
+ patch :toggle, params: { id: node.id }
+ }.to change { node.reload.folded }.to(true)
+ end
+ end
+
+ context 'with folded node' do
+ let(:node) { create(:alchemy_node, folded: true) }
+
+ it "expands node" do
+ expect {
+ patch :toggle, params: { id: node.id }
+ }.to change { node.reload.folded }.to(false)
+ end
+
+ context 'with node having children' do
+ before do
+ create(:alchemy_node, parent: node)
+ end
+
+ render_views
+
+ it "returns nodes children" do
+ patch :toggle, params: { id: node.id }
+ expect(response.body).to have_selector('li .sitemap_node')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/dummy/app/views/alchemy/menus/footer_navigation/_node.html.erb b/spec/dummy/app/views/alchemy/menus/footer_navigation/_node.html.erb
new file mode 100644
index 0000000000..ed71624a72
--- /dev/null
+++ b/spec/dummy/app/views/alchemy/menus/footer_navigation/_node.html.erb
@@ -0,0 +1,17 @@
+<%= content_tag :li, class: ['nav-item', node.children.any? ? 'dropdown' : nil].compact do %>
+ <%= link_to_if node.url,
+ node.name,
+ @preview_mode ? 'javascript: void(0)' : node.url,
+ class: ['nav-link', current_page?(node.url) ? 'active' : nil].compact,
+ title: node.title,
+ target: node.external? ? '_blank' : nil,
+ rel: node.nofollow? ? 'nofollow' : nil %>
+ <% if node.children.any? %>
+
+ <% end %>
+<% end %>
diff --git a/spec/dummy/app/views/alchemy/menus/footer_navigation/_wrapper.html.erb b/spec/dummy/app/views/alchemy/menus/footer_navigation/_wrapper.html.erb
new file mode 100644
index 0000000000..f314d3eedc
--- /dev/null
+++ b/spec/dummy/app/views/alchemy/menus/footer_navigation/_wrapper.html.erb
@@ -0,0 +1,6 @@
+
+ <%= render partial: options[:node_partial_name],
+ collection: node.children.includes(:page, :children),
+ locals: { options: options },
+ as: 'node' %>
+
diff --git a/spec/dummy/app/views/alchemy/menus/main_navigation/_node.html.erb b/spec/dummy/app/views/alchemy/menus/main_navigation/_node.html.erb
new file mode 100644
index 0000000000..ed71624a72
--- /dev/null
+++ b/spec/dummy/app/views/alchemy/menus/main_navigation/_node.html.erb
@@ -0,0 +1,17 @@
+<%= content_tag :li, class: ['nav-item', node.children.any? ? 'dropdown' : nil].compact do %>
+ <%= link_to_if node.url,
+ node.name,
+ @preview_mode ? 'javascript: void(0)' : node.url,
+ class: ['nav-link', current_page?(node.url) ? 'active' : nil].compact,
+ title: node.title,
+ target: node.external? ? '_blank' : nil,
+ rel: node.nofollow? ? 'nofollow' : nil %>
+ <% if node.children.any? %>
+
+ <% end %>
+<% end %>
diff --git a/spec/dummy/app/views/alchemy/menus/main_navigation/_wrapper.html.erb b/spec/dummy/app/views/alchemy/menus/main_navigation/_wrapper.html.erb
new file mode 100644
index 0000000000..f314d3eedc
--- /dev/null
+++ b/spec/dummy/app/views/alchemy/menus/main_navigation/_wrapper.html.erb
@@ -0,0 +1,6 @@
+
+ <%= render partial: options[:node_partial_name],
+ collection: node.children.includes(:page, :children),
+ locals: { options: options },
+ as: 'node' %>
+
diff --git a/spec/dummy/app/views/layouts/application.html.erb b/spec/dummy/app/views/layouts/application.html.erb
index 3179502411..17d080b35d 100644
--- a/spec/dummy/app/views/layouts/application.html.erb
+++ b/spec/dummy/app/views/layouts/application.html.erb
@@ -7,10 +7,10 @@
<%= csrf_meta_tags %>
+
<%= yield %>
-
- <%= render_navigation(:all_sub_menues => true) %>
-
<%= render "alchemy/edit_mode" %>