katalyst-navigation 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +25 -0
- data/app/assets/config/katalyst-navigation.js +1 -0
- data/app/assets/javascripts/controllers/navigation/editor/item_controller.js +45 -0
- data/app/assets/javascripts/controllers/navigation/editor/list_controller.js +105 -0
- data/app/assets/javascripts/controllers/navigation/editor/menu_controller.js +110 -0
- data/app/assets/javascripts/controllers/navigation/editor/new_item_controller.js +12 -0
- data/app/assets/javascripts/controllers/navigation/editor/status_bar_controller.js +22 -0
- data/app/assets/javascripts/utils/navigation/editor/item.js +245 -0
- data/app/assets/javascripts/utils/navigation/editor/menu.js +54 -0
- data/app/assets/javascripts/utils/navigation/editor/rules-engine.js +140 -0
- data/app/assets/stylesheets/katalyst/navigation/editor/_icon.scss +17 -0
- data/app/assets/stylesheets/katalyst/navigation/editor/_index.scss +145 -0
- data/app/assets/stylesheets/katalyst/navigation/editor/_item-actions.scss +92 -0
- data/app/assets/stylesheets/katalyst/navigation/editor/_item-rules.scss +24 -0
- data/app/assets/stylesheets/katalyst/navigation/editor/_new-items.scss +22 -0
- data/app/assets/stylesheets/katalyst/navigation/editor/_status-bar.scss +87 -0
- data/app/controllers/katalyst/navigation/base_controller.rb +12 -0
- data/app/controllers/katalyst/navigation/items_controller.rb +57 -0
- data/app/controllers/katalyst/navigation/menus_controller.rb +82 -0
- data/app/helpers/katalyst/navigation/editor/base.rb +41 -0
- data/app/helpers/katalyst/navigation/editor/item.rb +69 -0
- data/app/helpers/katalyst/navigation/editor/list.rb +41 -0
- data/app/helpers/katalyst/navigation/editor/menu.rb +37 -0
- data/app/helpers/katalyst/navigation/editor/new_item.rb +53 -0
- data/app/helpers/katalyst/navigation/editor/status_bar.rb +57 -0
- data/app/helpers/katalyst/navigation/editor_helper.rb +46 -0
- data/app/helpers/katalyst/navigation/frontend/builder.rb +53 -0
- data/app/helpers/katalyst/navigation/frontend_helper.rb +42 -0
- data/app/models/concerns/katalyst/navigation/garbage_collection.rb +31 -0
- data/app/models/concerns/katalyst/navigation/has_tree.rb +63 -0
- data/app/models/katalyst/navigation/button.rb +15 -0
- data/app/models/katalyst/navigation/item.rb +21 -0
- data/app/models/katalyst/navigation/link.rb +10 -0
- data/app/models/katalyst/navigation/menu.rb +123 -0
- data/app/models/katalyst/navigation/node.rb +21 -0
- data/app/models/katalyst/navigation/types/nodes_type.rb +42 -0
- data/app/views/katalyst/navigation/items/_button.html.erb +28 -0
- data/app/views/katalyst/navigation/items/_link.html.erb +21 -0
- data/app/views/katalyst/navigation/items/edit.html.erb +4 -0
- data/app/views/katalyst/navigation/items/new.html.erb +4 -0
- data/app/views/katalyst/navigation/items/update.turbo_stream.erb +7 -0
- data/app/views/katalyst/navigation/menus/_item.html.erb +15 -0
- data/app/views/katalyst/navigation/menus/_list_item.html.erb +14 -0
- data/app/views/katalyst/navigation/menus/_new_item.html.erb +3 -0
- data/app/views/katalyst/navigation/menus/_new_items.html.erb +5 -0
- data/app/views/katalyst/navigation/menus/edit.html.erb +15 -0
- data/app/views/katalyst/navigation/menus/index.html.erb +17 -0
- data/app/views/katalyst/navigation/menus/new.html.erb +15 -0
- data/app/views/katalyst/navigation/menus/show.html.erb +15 -0
- data/config/importmap.rb +5 -0
- data/config/locales/en.yml +12 -0
- data/config/routes.rb +9 -0
- data/db/migrate/20220826034057_create_katalyst_navigation_menus.rb +25 -0
- data/db/migrate/20220826034507_create_katalyst_navigation_items.rb +17 -0
- data/lib/katalyst/navigation/engine.rb +36 -0
- data/lib/katalyst/navigation/version.rb +7 -0
- data/lib/katalyst/navigation.rb +9 -0
- data/lib/tasks/yarn.rake +18 -0
- data/spec/factories/katalyst/navigation/items.rb +14 -0
- data/spec/factories/katalyst/navigation/menus.rb +17 -0
- metadata +109 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Navigation
|
5
|
+
module Editor
|
6
|
+
class Base
|
7
|
+
MENU_CONTROLLER = "navigation--editor--menu"
|
8
|
+
LIST_CONTROLLER = "navigation--editor--list"
|
9
|
+
ITEM_CONTROLLER = "navigation--editor--item"
|
10
|
+
STATUS_BAR_CONTROLLER = "navigation--editor--status-bar"
|
11
|
+
NEW_ITEM_CONTROLLER = "navigation--editor--new-item"
|
12
|
+
|
13
|
+
ATTRIBUTES_SCOPE = "menu[items_attributes][]"
|
14
|
+
TURBO_FRAME = "navigation--editor--item-frame"
|
15
|
+
|
16
|
+
attr_accessor :template, :menu
|
17
|
+
|
18
|
+
delegate_missing_to :template
|
19
|
+
|
20
|
+
def initialize(template, menu)
|
21
|
+
self.template = template
|
22
|
+
self.menu = menu
|
23
|
+
end
|
24
|
+
|
25
|
+
def menu_form_id
|
26
|
+
dom_id(menu, :items)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def add_option(options, key, *path)
|
32
|
+
if path.length > 1
|
33
|
+
add_option(options[key] ||= {}, *path)
|
34
|
+
else
|
35
|
+
options[key] = [options[key], *path].compact.join(" ")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Navigation
|
5
|
+
module Editor
|
6
|
+
class Item < Base
|
7
|
+
attr_accessor :item
|
8
|
+
|
9
|
+
def build(item, **options, &block)
|
10
|
+
self.item = item
|
11
|
+
content_tag(:div, default_options(id: dom_id(item), **options)) do
|
12
|
+
concat(capture { yield self }) if block
|
13
|
+
concat fields(item)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def build_all(*items)
|
18
|
+
render partial: "katalyst/navigation/menus/link_item",
|
19
|
+
layout: "katalyst/navigation/menus/navigation_menu_link",
|
20
|
+
collection: items,
|
21
|
+
as: :item
|
22
|
+
end
|
23
|
+
|
24
|
+
def accordion_actions
|
25
|
+
tag.div role: "toolbar", data: { tree_accordion_controls: "" } do
|
26
|
+
concat tag.span(role: "button", value: "collapse",
|
27
|
+
data: { action: "click->#{MENU_CONTROLLER}#collapse", title: "Collapse tree" })
|
28
|
+
concat tag.span(role: "button", value: "expand",
|
29
|
+
data: { action: "click->#{MENU_CONTROLLER}#expand", title: "Expand tree" })
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def item_actions
|
34
|
+
tag.div role: "toolbar", data: { tree_controls: "" } do
|
35
|
+
concat tag.span(role: "button", value: "de-nest",
|
36
|
+
data: { action: "click->#{MENU_CONTROLLER}#deNest", title: "Outdent" })
|
37
|
+
concat tag.span(role: "button", value: "nest",
|
38
|
+
data: { action: "click->#{MENU_CONTROLLER}#nest", title: "Indent" })
|
39
|
+
concat link_to("", edit_item_link,
|
40
|
+
role: "button", title: "Edit", value: "edit",
|
41
|
+
data: { turbo_frame: TURBO_FRAME })
|
42
|
+
concat tag.span(role: "button", value: "remove",
|
43
|
+
data: { action: "click->#{MENU_CONTROLLER}#remove", title: "Remove" })
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def edit_item_link
|
48
|
+
item.persisted? ? edit_menu_item_path(item.menu, item) : new_menu_item_path(item.menu, type: item.type)
|
49
|
+
end
|
50
|
+
|
51
|
+
def fields(item)
|
52
|
+
template.fields(ATTRIBUTES_SCOPE, model: item, index: nil, skip_default_ids: true) do |f|
|
53
|
+
concat f.hidden_field(:id)
|
54
|
+
concat f.hidden_field(:depth)
|
55
|
+
concat f.hidden_field(:index)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def default_options(options)
|
62
|
+
add_option(options, :data, :controller, ITEM_CONTROLLER)
|
63
|
+
|
64
|
+
options
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Navigation
|
5
|
+
module Editor
|
6
|
+
class List < Base
|
7
|
+
ACTIONS = <<~ACTIONS.gsub(/\s+/, " ").freeze
|
8
|
+
dragstart->#{LIST_CONTROLLER}#dragstart
|
9
|
+
dragover->#{LIST_CONTROLLER}#dragover
|
10
|
+
dragenter->#{LIST_CONTROLLER}#dragenter
|
11
|
+
dragleave->#{LIST_CONTROLLER}#dragleave
|
12
|
+
drop->#{LIST_CONTROLLER}#drop
|
13
|
+
dragend->#{LIST_CONTROLLER}#dragend
|
14
|
+
ACTIONS
|
15
|
+
|
16
|
+
def build(options, &_block)
|
17
|
+
content_tag :ol, default_options(id: menu_form_id, **options) do
|
18
|
+
yield self
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def items(*items)
|
23
|
+
render partial: "katalyst/navigation/menus/item",
|
24
|
+
layout: "katalyst/navigation/menus/list_item",
|
25
|
+
collection: items,
|
26
|
+
as: :item
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def default_options(options)
|
32
|
+
add_option(options, :data, :controller, LIST_CONTROLLER)
|
33
|
+
add_option(options, :data, :action, ACTIONS)
|
34
|
+
add_option(options, :data, :"#{MENU_CONTROLLER}_target", "menu")
|
35
|
+
|
36
|
+
options
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Navigation
|
5
|
+
module Editor
|
6
|
+
class Menu < Base
|
7
|
+
ACTIONS = <<~ACTIONS.gsub(/\s+/, " ").freeze
|
8
|
+
submit->#{MENU_CONTROLLER}#reindex
|
9
|
+
navigation:reindex->#{MENU_CONTROLLER}#reindex
|
10
|
+
navigation:reset->#{MENU_CONTROLLER}#reset
|
11
|
+
ACTIONS
|
12
|
+
|
13
|
+
def build(options)
|
14
|
+
form_with(model: menu, **default_options(id: menu_form_id, **options)) do |form|
|
15
|
+
concat hidden_input
|
16
|
+
concat(capture { yield form })
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
# Hidden input ensures that if the menu is empty then the controller
|
23
|
+
# receives an empty array.
|
24
|
+
def hidden_input
|
25
|
+
tag.input(type: "hidden", name: "#{Item::ATTRIBUTES_SCOPE}[id]")
|
26
|
+
end
|
27
|
+
|
28
|
+
def default_options(options)
|
29
|
+
add_option(options, :data, :controller, MENU_CONTROLLER)
|
30
|
+
add_option(options, :data, :action, ACTIONS)
|
31
|
+
|
32
|
+
options
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Navigation
|
5
|
+
module Editor
|
6
|
+
class NewItem < Base
|
7
|
+
ACTIONS = <<~ACTIONS.gsub(/\s+/, " ").freeze
|
8
|
+
dragstart->#{NEW_ITEM_CONTROLLER}#dragstart
|
9
|
+
ACTIONS
|
10
|
+
|
11
|
+
def build(item, **options, &block)
|
12
|
+
capture do
|
13
|
+
concat(content_tag(:div, **default_options(options)) do
|
14
|
+
concat capture(&block)
|
15
|
+
concat item_template(item)
|
16
|
+
end)
|
17
|
+
concat turbo_replace_placeholder(item)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Remove items that are incomplete when rendering new items, this
|
22
|
+
# causes incomplete items to be removed from the list when the user
|
23
|
+
# cancels adding a new item by pressing 'discard' in the new item form.
|
24
|
+
def turbo_replace_placeholder(item)
|
25
|
+
turbo_stream.replace dom_id(item) do
|
26
|
+
navigation_editor_item(item: item, data: { delete: "" })
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Template is stored inside the new item dom, and copied into drag
|
31
|
+
# events when the user initiates drag so that it can be copied into the
|
32
|
+
# editor list on drop.
|
33
|
+
def item_template(item)
|
34
|
+
content_tag(:template, data: { "#{NEW_ITEM_CONTROLLER}-target" => "template" }) do
|
35
|
+
navigation_editor_items(item: item)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def default_options(options)
|
42
|
+
add_option(options, :draggable, true)
|
43
|
+
add_option(options, :role, "listitem")
|
44
|
+
add_option(options, :data, :turbo_frame, TURBO_FRAME)
|
45
|
+
add_option(options, :data, :controller, NEW_ITEM_CONTROLLER)
|
46
|
+
add_option(options, :data, :action, ACTIONS)
|
47
|
+
|
48
|
+
options
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Navigation
|
5
|
+
module Editor
|
6
|
+
class StatusBar < Base
|
7
|
+
ACTIONS = <<~ACTIONS.gsub(/\s+/, " ").freeze
|
8
|
+
navigation:change@document->#{STATUS_BAR_CONTROLLER}#change
|
9
|
+
ACTIONS
|
10
|
+
|
11
|
+
def build(**options)
|
12
|
+
content_tag(:div, default_options(**options)) do
|
13
|
+
concat status(:published, last_update: l(menu.updated_at, format: :short))
|
14
|
+
concat status(:draft)
|
15
|
+
concat status(:dirty)
|
16
|
+
concat actions
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def status(state, **options)
|
21
|
+
tag.span(t("views.katalyst.navigation.editor.#{state}_html", **options),
|
22
|
+
class: "status-text",
|
23
|
+
data: { state => "" })
|
24
|
+
end
|
25
|
+
|
26
|
+
def actions
|
27
|
+
content_tag(:menu) do
|
28
|
+
concat action(:discard, class: "button button--text")
|
29
|
+
concat action(:revert, class: "button button--text") if menu.state == :draft
|
30
|
+
concat action(:save, class: "button button--secondary")
|
31
|
+
concat action(:publish, class: "button button--primary")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def action(action, **options)
|
36
|
+
tag.li do
|
37
|
+
button_tag(t("views.katalyst.navigation.editor.#{action}"),
|
38
|
+
name: "commit",
|
39
|
+
value: action,
|
40
|
+
form: menu_form_id,
|
41
|
+
**options)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def default_options(**options)
|
48
|
+
add_option(options, :data, :controller, STATUS_BAR_CONTROLLER)
|
49
|
+
add_option(options, :data, :action, ACTIONS)
|
50
|
+
add_option(options, :data, :state, menu.state)
|
51
|
+
|
52
|
+
options
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Navigation
|
5
|
+
module EditorHelper
|
6
|
+
def navigation_editor_new_items(menu)
|
7
|
+
[
|
8
|
+
Link.new(menu: menu),
|
9
|
+
Button.new(menu: menu),
|
10
|
+
]
|
11
|
+
end
|
12
|
+
|
13
|
+
def navigation_editor_menu(menu:, **options, &block)
|
14
|
+
Editor::Menu.new(self, menu).build(options, &block)
|
15
|
+
end
|
16
|
+
|
17
|
+
def navigation_editor_list(menu:, items: menu.draft_items, **options)
|
18
|
+
Editor::List.new(self, menu).build(options) do |list|
|
19
|
+
list.items(*items) if items.present?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Generate items without their list wrapper, similar to form_with/fields
|
24
|
+
def navigation_editor_items(item:, menu: item.menu)
|
25
|
+
Editor::List.new(self, menu).items(item)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Gene
|
29
|
+
def navigation_editor_new_item(item:, menu: item.menu, **options, &block)
|
30
|
+
Editor::NewItem.new(self, menu).build(item, **options, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def navigation_editor_item(item:, menu: item.menu, **options, &block)
|
34
|
+
Editor::Item.new(self, menu).build(item, **options, &block)
|
35
|
+
end
|
36
|
+
|
37
|
+
def navigation_editor_item_fields(item:, menu: item.menu)
|
38
|
+
Editor::Item.new(self, menu).fields(item)
|
39
|
+
end
|
40
|
+
|
41
|
+
def navigation_editor_status_bar(menu:, **options)
|
42
|
+
Editor::StatusBar.new(self, menu).build(**options)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Navigation
|
5
|
+
module Frontend
|
6
|
+
class Builder
|
7
|
+
attr_accessor :template, :menu_options, :list_options, :item_options
|
8
|
+
|
9
|
+
delegate_missing_to :@template
|
10
|
+
|
11
|
+
def initialize(template, menu: {}, list: {}, item: {})
|
12
|
+
self.template = template
|
13
|
+
self.menu_options = menu
|
14
|
+
self.list_options = list
|
15
|
+
self.item_options = item
|
16
|
+
end
|
17
|
+
|
18
|
+
def render(tree)
|
19
|
+
content_tag(:ul, menu_options) do
|
20
|
+
tree.each do |item|
|
21
|
+
concat render_item(item)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def render_item(item)
|
27
|
+
return unless item.visible?
|
28
|
+
|
29
|
+
content_tag :li, item_options do
|
30
|
+
concat public_send("render_#{item.model_name.param_key}", item)
|
31
|
+
concat render_list(item.children) if item.children.any?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def render_list(items)
|
36
|
+
content_tag :ul, list_options do
|
37
|
+
items.each do |child|
|
38
|
+
concat render_item(child)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def render_link(link)
|
44
|
+
link_to(link.title, link.url, item_options)
|
45
|
+
end
|
46
|
+
|
47
|
+
def render_button(link)
|
48
|
+
link_to(link.title, link.url, **item_options, method: link.http_method)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Navigation
|
5
|
+
module FrontendHelper
|
6
|
+
mattr_accessor :navigation_builder
|
7
|
+
|
8
|
+
# Render a navigation menu. Caches based on the published version's id.
|
9
|
+
#
|
10
|
+
# @param(menu: Katalyst::Navigation::Menu)
|
11
|
+
# @return Structured HTML containing top level + nested navigation links
|
12
|
+
def render_navigation_menu(menu, item: {}, list: {}, **options)
|
13
|
+
return unless menu&.published_version&.present?
|
14
|
+
|
15
|
+
cache menu.published_version do
|
16
|
+
builder = default_navigation_builder_class.new(self, menu: options, item: item, list: list)
|
17
|
+
concat builder.render(menu.published_tree)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Render items without a wrapper list, useful for inline rendering of items
|
22
|
+
#
|
23
|
+
# @param(items: [Katalyst::Navigation::Item])
|
24
|
+
# @return Structured HTML containing top level + nested navigation links
|
25
|
+
def render_navigation_items(items, list: {}, **options)
|
26
|
+
builder = default_navigation_builder_class.new(self, item: options, list: list)
|
27
|
+
capture do
|
28
|
+
items.each do |item|
|
29
|
+
concat builder.render_item(item)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def default_navigation_builder_class
|
37
|
+
builder = controller.try(:default_navigation_builder) || Frontend::Builder
|
38
|
+
builder.respond_to?(:constantize) ? builder.constantize : builder
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Navigation
|
5
|
+
module GarbageCollection
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
after_update :remove_stale_versions
|
10
|
+
end
|
11
|
+
|
12
|
+
def remove_stale_versions
|
13
|
+
transaction do
|
14
|
+
# find all the versions that are not linked to the menu
|
15
|
+
orphaned_versions = versions.inactive
|
16
|
+
|
17
|
+
next unless orphaned_versions.any?
|
18
|
+
|
19
|
+
# find links that are not included in active versions
|
20
|
+
orphaned_items = items.pluck(:id) - versions.active.pluck(:nodes).flat_map { |k| k.map(&:id) }.uniq
|
21
|
+
|
22
|
+
# delete orphaned links with a 2 hour grace period to allow for in-progress editing
|
23
|
+
items.where(id: orphaned_items, updated_at: ..2.hours.ago).destroy_all
|
24
|
+
|
25
|
+
# delete orphaned versions without a grace period as they can only be created by updates
|
26
|
+
orphaned_versions.destroy_all
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Navigation
|
5
|
+
module HasTree
|
6
|
+
# Constructs a tree from an ordered list of items with depth.
|
7
|
+
# * items that have higher depth than their predecessor are nested as `children`.
|
8
|
+
# * items with the same depth become siblings.
|
9
|
+
def tree
|
10
|
+
items.reduce(Builder.new, &:add).tree
|
11
|
+
end
|
12
|
+
|
13
|
+
class Builder
|
14
|
+
attr_reader :current, :depth, :children, :tree
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@depth = 0
|
18
|
+
@children = @tree = []
|
19
|
+
end
|
20
|
+
|
21
|
+
def add(node)
|
22
|
+
if node.depth == depth
|
23
|
+
node.parent = current
|
24
|
+
children << node
|
25
|
+
self
|
26
|
+
elsif node.depth > depth
|
27
|
+
push(children.last)
|
28
|
+
add(node)
|
29
|
+
else
|
30
|
+
pop
|
31
|
+
add(node)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
attr_writer :current, :depth, :children
|
38
|
+
|
39
|
+
# Add node to the top of builder stacks
|
40
|
+
def push(node)
|
41
|
+
self.depth += 1
|
42
|
+
self.current = node
|
43
|
+
self.children = node.children = []
|
44
|
+
node
|
45
|
+
end
|
46
|
+
|
47
|
+
# Remove current from builder stack
|
48
|
+
def pop
|
49
|
+
previous = current
|
50
|
+
self.depth -= 1
|
51
|
+
if depth.zero?
|
52
|
+
self.current = nil
|
53
|
+
self.children = tree
|
54
|
+
else
|
55
|
+
self.current = previous.parent
|
56
|
+
self.children = current.children
|
57
|
+
end
|
58
|
+
previous
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Navigation
|
5
|
+
# Renders an HTML button using `button_to`.
|
6
|
+
class Button < Item
|
7
|
+
HTTP_METHODS = %i[get post patch put delete].index_by(&:itself).freeze
|
8
|
+
|
9
|
+
enum method: HTTP_METHODS, _prefix: :http
|
10
|
+
|
11
|
+
validates :title, :url, :http_method, presence: true
|
12
|
+
validates :http_method, inclusion: { in: HTTP_METHODS.values.map(&:to_s) }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Navigation
|
5
|
+
# STI base class for menu items (links and buttons)
|
6
|
+
class Item < ApplicationRecord
|
7
|
+
belongs_to :menu, inverse_of: :items, class_name: "Katalyst::Navigation::Menu"
|
8
|
+
|
9
|
+
after_initialize :initialize_tree
|
10
|
+
|
11
|
+
attr_accessor :parent, :children, :index, :depth
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def initialize_tree
|
16
|
+
self.parent ||= nil
|
17
|
+
self.children ||= []
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|