journeybook 0.1.6.pre.alpha
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 +9 -0
- data/MIT-LICENSE +20 -0
- data/README.md +354 -0
- data/Rakefile +6 -0
- data/app/assets/images/journeybook/.keep +0 -0
- data/app/assets/images/journeybook/logo.svg +5 -0
- data/app/assets/javascripts/journeybook/sortable.js +96 -0
- data/app/assets/stylesheets/journeybook/application.css +1556 -0
- data/app/controllers/concerns/.keep +0 -0
- data/app/controllers/concerns/journeybook/public_pages.rb +31 -0
- data/app/controllers/journeybook/admin/ai_pages_controller.rb +65 -0
- data/app/controllers/journeybook/admin/ai_style_prompts_controller.rb +51 -0
- data/app/controllers/journeybook/admin/components_controller.rb +57 -0
- data/app/controllers/journeybook/admin/dashboard_controller.rb +13 -0
- data/app/controllers/journeybook/admin/media_assets_controller.rb +59 -0
- data/app/controllers/journeybook/admin/navigation_items_controller.rb +91 -0
- data/app/controllers/journeybook/admin/navigation_menus_controller.rb +59 -0
- data/app/controllers/journeybook/admin/pages_controller.rb +304 -0
- data/app/controllers/journeybook/admin/redirects_controller.rb +51 -0
- data/app/controllers/journeybook/admin/sites_controller.rb +51 -0
- data/app/controllers/journeybook/application_controller.rb +52 -0
- data/app/controllers/journeybook/form_submissions_controller.rb +55 -0
- data/app/helpers/journeybook/application_helper.rb +4 -0
- data/app/helpers/journeybook/navigation_helper.rb +11 -0
- data/app/javascript/journeybook/application.js +5 -0
- data/app/javascript/journeybook/component_search_controller.js +13 -0
- data/app/jobs/journeybook/application_job.rb +4 -0
- data/app/models/concerns/.keep +0 -0
- data/app/models/journeybook/ai_style_prompt.rb +6 -0
- data/app/models/journeybook/application_record.rb +5 -0
- data/app/models/journeybook/media_asset.rb +26 -0
- data/app/models/journeybook/navigation_item.rb +45 -0
- data/app/models/journeybook/navigation_menu.rb +23 -0
- data/app/models/journeybook/page.rb +96 -0
- data/app/models/journeybook/redirect.rb +31 -0
- data/app/models/journeybook/site.rb +62 -0
- data/app/services/journeybook/ai/markdown_generator.rb +15 -0
- data/app/services/journeybook/ai/openai_client.rb +54 -0
- data/app/services/journeybook/ai/prompt_builder.rb +154 -0
- data/app/services/journeybook/ai/template_validator.rb +17 -0
- data/app/services/journeybook/component_parser.rb +53 -0
- data/app/services/journeybook/component_renderer.rb +108 -0
- data/app/services/journeybook/component_tag_updater.rb +132 -0
- data/app/services/journeybook/component_validator.rb +111 -0
- data/app/services/journeybook/form_mailer.rb +69 -0
- data/app/services/journeybook/html_sanitizer.rb +46 -0
- data/app/services/journeybook/letter_opener_client.rb +14 -0
- data/app/services/journeybook/markdown_renderer.rb +29 -0
- data/app/services/journeybook/page_renderer.rb +58 -0
- data/app/services/journeybook/page_resolver.rb +55 -0
- data/app/services/journeybook/postmark_client.rb +48 -0
- data/app/services/journeybook/storage/s3_client.rb +96 -0
- data/app/views/journeybook/admin/ai_pages/_draft_form.html.erb +73 -0
- data/app/views/journeybook/admin/ai_pages/new.html.erb +43 -0
- data/app/views/journeybook/admin/ai_style_prompts/_form.html.erb +16 -0
- data/app/views/journeybook/admin/ai_style_prompts/edit.html.erb +2 -0
- data/app/views/journeybook/admin/ai_style_prompts/index.html.erb +26 -0
- data/app/views/journeybook/admin/ai_style_prompts/new.html.erb +2 -0
- data/app/views/journeybook/admin/components/_component.html.erb +17 -0
- data/app/views/journeybook/admin/components/_reference.html.erb +9 -0
- data/app/views/journeybook/admin/components/index.html.erb +46 -0
- data/app/views/journeybook/admin/components/preview.html.erb +13 -0
- data/app/views/journeybook/admin/dashboard/show.html.erb +34 -0
- data/app/views/journeybook/admin/media_assets/_reference.html.erb +16 -0
- data/app/views/journeybook/admin/media_assets/index.html.erb +43 -0
- data/app/views/journeybook/admin/media_assets/new.html.erb +41 -0
- data/app/views/journeybook/admin/navigation_items/_form.html.erb +46 -0
- data/app/views/journeybook/admin/navigation_items/edit.html.erb +2 -0
- data/app/views/journeybook/admin/navigation_items/new.html.erb +2 -0
- data/app/views/journeybook/admin/navigation_menus/_form.html.erb +10 -0
- data/app/views/journeybook/admin/navigation_menus/edit.html.erb +38 -0
- data/app/views/journeybook/admin/navigation_menus/index.html.erb +28 -0
- data/app/views/journeybook/admin/navigation_menus/new.html.erb +2 -0
- data/app/views/journeybook/admin/pages/_collection_field.html.erb +58 -0
- data/app/views/journeybook/admin/pages/_form.html.erb +77 -0
- data/app/views/journeybook/admin/pages/_image_picker_modal.html.erb +23 -0
- data/app/views/journeybook/admin/pages/_table.html.erb +37 -0
- data/app/views/journeybook/admin/pages/_url_field.html.erb +25 -0
- data/app/views/journeybook/admin/pages/edit.html.erb +547 -0
- data/app/views/journeybook/admin/pages/errors.html.erb +25 -0
- data/app/views/journeybook/admin/pages/index.html.erb +52 -0
- data/app/views/journeybook/admin/pages/new.html.erb +27 -0
- data/app/views/journeybook/admin/pages/new_component.html.erb +70 -0
- data/app/views/journeybook/admin/pages/public_preview.html.erb +27 -0
- data/app/views/journeybook/admin/pages/source.html.erb +23 -0
- data/app/views/journeybook/admin/redirects/_form.html.erb +9 -0
- data/app/views/journeybook/admin/redirects/edit.html.erb +2 -0
- data/app/views/journeybook/admin/redirects/index.html.erb +15 -0
- data/app/views/journeybook/admin/redirects/new.html.erb +2 -0
- data/app/views/journeybook/admin/shared/_drag_handle.html.erb +3 -0
- data/app/views/journeybook/admin/shared/_errors.html.erb +10 -0
- data/app/views/journeybook/admin/sites/_form.html.erb +12 -0
- data/app/views/journeybook/admin/sites/edit.html.erb +2 -0
- data/app/views/journeybook/admin/sites/index.html.erb +26 -0
- data/app/views/journeybook/admin/sites/new.html.erb +2 -0
- data/app/views/layouts/journeybook/application.html.erb +57 -0
- data/config/importmap.rb +3 -0
- data/config/routes.rb +39 -0
- data/db/migrate/20260627000001_create_journeybook_sites.rb +13 -0
- data/db/migrate/20260627000002_create_journeybook_pages.rb +23 -0
- data/db/migrate/20260627000003_create_journeybook_redirects.rb +13 -0
- data/db/migrate/20260628000004_add_context_fields_to_journeybook_sites.rb +38 -0
- data/db/migrate/20260628000005_create_journeybook_media_assets.rb +24 -0
- data/db/migrate/20260629000006_create_journeybook_navigation.rb +28 -0
- data/db/migrate/20260731000007_add_open_in_new_window_to_journeybook_navigation_items.rb +5 -0
- data/db/migrate/20260731000008_create_journeybook_ai_style_prompts.rb +12 -0
- data/db/migrate/20260801000009_add_blog_fields_to_journeybook_pages.rb +9 -0
- data/db/migrate/20260801000010_add_knowledge_base_fields_to_journeybook_pages.rb +8 -0
- data/db/migrate/20260801000011_add_position_to_journeybook_pages.rb +6 -0
- data/lib/generators/journeybook/install/install_generator.rb +72 -0
- data/lib/generators/journeybook/install/templates/add_blog_fields_to_journeybook_pages.rb +9 -0
- data/lib/generators/journeybook/install/templates/add_context_fields_to_journeybook_sites.rb +38 -0
- data/lib/generators/journeybook/install/templates/add_knowledge_base_fields_to_journeybook_pages.rb +8 -0
- data/lib/generators/journeybook/install/templates/add_open_in_new_window_to_journeybook_navigation_items.rb +5 -0
- data/lib/generators/journeybook/install/templates/add_position_to_journeybook_pages.rb +6 -0
- data/lib/generators/journeybook/install/templates/create_journeybook_ai_style_prompts.rb +12 -0
- data/lib/generators/journeybook/install/templates/create_journeybook_media_assets.rb +24 -0
- data/lib/generators/journeybook/install/templates/create_journeybook_navigation.rb +28 -0
- data/lib/generators/journeybook/install/templates/create_journeybook_pages.rb +33 -0
- data/lib/generators/journeybook/install/templates/create_journeybook_redirects.rb +13 -0
- data/lib/generators/journeybook/install/templates/create_journeybook_sites.rb +13 -0
- data/lib/generators/journeybook/install/templates/journeybook.rb +42 -0
- data/lib/generators/journeybook/install/templates/pages_controller.rb +3 -0
- data/lib/generators/journeybook/install/templates/show.html.erb +10 -0
- data/lib/journeybook/component_registry.rb +136 -0
- data/lib/journeybook/configuration.rb +55 -0
- data/lib/journeybook/engine.rb +19 -0
- data/lib/journeybook/page_template_registry.rb +67 -0
- data/lib/journeybook/public_page_constraint.rb +23 -0
- data/lib/journeybook/version.rb +3 -0
- data/lib/journeybook.rb +62 -0
- data/lib/tasks/journeybook_tasks.rake +8 -0
- metadata +266 -0
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Journeybook
|
|
2
|
+
module PublicPages
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
helper Journeybook::NavigationHelper
|
|
7
|
+
helper_method :journeybook_page, :journeybook_html, :journeybook_site
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def show
|
|
11
|
+
resolver = Journeybook::PageResolver.from_request(request, path: params[:path])
|
|
12
|
+
@journeybook_page = resolver.call
|
|
13
|
+
|
|
14
|
+
redirect = Journeybook::Redirect.for_path(params[:path]) if @journeybook_page.blank? && resolver.site
|
|
15
|
+
return redirect_to(redirect.to, status: redirect.status_code) if redirect
|
|
16
|
+
|
|
17
|
+
return render_not_found unless @journeybook_page
|
|
18
|
+
|
|
19
|
+
@journeybook_site = @journeybook_page.site
|
|
20
|
+
@journeybook_html = Journeybook::PageRenderer.new.render(@journeybook_page, form_authenticity_token: form_authenticity_token)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
attr_reader :journeybook_page, :journeybook_html, :journeybook_site
|
|
26
|
+
|
|
27
|
+
def render_not_found
|
|
28
|
+
render file: Rails.public_path.join("404.html"), layout: false, status: :not_found
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module Journeybook
|
|
2
|
+
module Admin
|
|
3
|
+
class AiPagesController < ApplicationController
|
|
4
|
+
before_action :set_sites
|
|
5
|
+
|
|
6
|
+
def new
|
|
7
|
+
@page = Page.new(site: current_site || @sites.first, locale: current_site&.default_locale || "en", status: "draft")
|
|
8
|
+
set_templates
|
|
9
|
+
set_style_prompts
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def create
|
|
13
|
+
@page = Page.new(page_params)
|
|
14
|
+
@selected_template = template_for(params[:template_key])
|
|
15
|
+
@page.page_type = @selected_template.page_type if @selected_template
|
|
16
|
+
@prompt = params[:prompt].to_s
|
|
17
|
+
@style_prompt = AiStylePrompt.find_by(id: params[:ai_style_prompt_id])
|
|
18
|
+
set_templates
|
|
19
|
+
set_style_prompts
|
|
20
|
+
|
|
21
|
+
if @prompt.blank?
|
|
22
|
+
@page.errors.add(:base, "Prompt is required.")
|
|
23
|
+
return render :new, status: :unprocessable_entity
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
@page.markdown = Ai::MarkdownGenerator.new(site: @page.site, style_prompt: @style_prompt&.prompt, template: @selected_template).generate(prompt: @prompt)
|
|
27
|
+
add_generation_errors
|
|
28
|
+
render :new
|
|
29
|
+
rescue Ai::OpenaiClient::Error => error
|
|
30
|
+
@page.errors.add(:base, error.message)
|
|
31
|
+
render :new, status: :unprocessable_entity
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def set_sites
|
|
37
|
+
@sites = Site.order(:name)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def set_style_prompts
|
|
41
|
+
@style_prompts = AiStylePrompt.order(:name)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def set_templates
|
|
45
|
+
@templates = Journeybook.templates.for_site(@page&.site || current_site || @sites.first)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def template_for(key)
|
|
49
|
+
return unless key.present?
|
|
50
|
+
|
|
51
|
+
Journeybook.templates.for_site(@page.site).find { |template| template.key.to_s == key.to_s }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def add_generation_errors
|
|
55
|
+
errors = @page.component_validation_errors
|
|
56
|
+
errors.concat(Journeybook::Ai::TemplateValidator.new.errors_for(@page.markdown, @selected_template)) if @selected_template
|
|
57
|
+
errors.uniq.each { |message| @page.errors.add(:markdown, message) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def page_params
|
|
61
|
+
params.fetch(:page, {}).permit(:site_id, :title, :path, :meta_title, :meta_description, :status, :locale, :page_type, :excerpt, :author_name, :topic).with_defaults(site_id: current_site&.id, status: "draft", locale: current_site&.default_locale || "en")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module Journeybook
|
|
2
|
+
module Admin
|
|
3
|
+
class AiStylePromptsController < ApplicationController
|
|
4
|
+
before_action :set_style_prompt, only: %i[edit update destroy]
|
|
5
|
+
|
|
6
|
+
def index
|
|
7
|
+
@style_prompts = AiStylePrompt.order(:name)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def new
|
|
11
|
+
@style_prompt = AiStylePrompt.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create
|
|
15
|
+
@style_prompt = AiStylePrompt.new(style_prompt_params)
|
|
16
|
+
|
|
17
|
+
if @style_prompt.save
|
|
18
|
+
redirect_to admin_ai_style_prompts_path, notice: "AI style prompt created."
|
|
19
|
+
else
|
|
20
|
+
render :new, status: :unprocessable_entity
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def edit
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def update
|
|
28
|
+
if @style_prompt.update(style_prompt_params)
|
|
29
|
+
redirect_to admin_ai_style_prompts_path, notice: "AI style prompt updated."
|
|
30
|
+
else
|
|
31
|
+
render :edit, status: :unprocessable_entity
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def destroy
|
|
36
|
+
@style_prompt.destroy
|
|
37
|
+
redirect_to admin_ai_style_prompts_path, notice: "AI style prompt deleted."
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def set_style_prompt
|
|
43
|
+
@style_prompt = AiStylePrompt.find(params[:id])
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def style_prompt_params
|
|
47
|
+
params.require(:ai_style_prompt).permit(:name, :prompt)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module Journeybook
|
|
2
|
+
module Admin
|
|
3
|
+
class ComponentsController < ApplicationController
|
|
4
|
+
def index
|
|
5
|
+
@components = Journeybook.components.for_site(current_site).to_a
|
|
6
|
+
@selected_component = selected_component
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def preview
|
|
10
|
+
@components = Journeybook.components.for_site(current_site).to_a
|
|
11
|
+
@selected_component = component_for(params[:component])
|
|
12
|
+
return head :not_found unless @selected_component
|
|
13
|
+
|
|
14
|
+
@preview_props = preview_props(@selected_component)
|
|
15
|
+
@preview_html = render_preview(@selected_component, @preview_props)
|
|
16
|
+
|
|
17
|
+
render :preview, layout: preview_layout
|
|
18
|
+
rescue StandardError => error
|
|
19
|
+
@preview_error = "#{error.class}: #{error.message}"
|
|
20
|
+
render :preview, layout: preview_layout, status: :unprocessable_entity
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def selected_component
|
|
26
|
+
return if @components.empty?
|
|
27
|
+
|
|
28
|
+
component_for(params[:component]) || @components.first
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def component_for(name)
|
|
32
|
+
@components.find { |component| component.name.to_s == name.to_s }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def preview_props(component)
|
|
36
|
+
component_class = component.component_class
|
|
37
|
+
return component_class.preview_props if component_class.respond_to?(:preview_props)
|
|
38
|
+
|
|
39
|
+
raise ArgumentError, "#{component.name} must define .preview_props for its component preview."
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def render_preview(component, props)
|
|
43
|
+
component_class = component.component_class
|
|
44
|
+
html = if component_class.respond_to?(:render)
|
|
45
|
+
component_class.render(**props)
|
|
46
|
+
else
|
|
47
|
+
component_class.new(**props).render
|
|
48
|
+
end
|
|
49
|
+
HtmlSanitizer.new.sanitize(html)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def preview_layout
|
|
53
|
+
current_site&.layout_name || Journeybook.configuration.default_layout
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Journeybook
|
|
2
|
+
module Admin
|
|
3
|
+
class DashboardController < ApplicationController
|
|
4
|
+
def show
|
|
5
|
+
@site_count = Site.count
|
|
6
|
+
@page_count = current_site ? current_site.pages.count : 0
|
|
7
|
+
@published_page_count = current_site ? current_site.pages.published.count : 0
|
|
8
|
+
@component_count = Journeybook.components.for_site(current_site).count
|
|
9
|
+
@recent_pages = current_site ? current_site.pages.includes(:site).order(updated_at: :desc).limit(5) : Page.none
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module Journeybook
|
|
2
|
+
module Admin
|
|
3
|
+
class MediaAssetsController < ApplicationController
|
|
4
|
+
rescue_from Journeybook::Storage::S3Client::Error, with: :storage_error
|
|
5
|
+
|
|
6
|
+
def index
|
|
7
|
+
@media_asset = MediaAsset.new
|
|
8
|
+
@media_assets = MediaAsset.order(created_at: :desc)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def new
|
|
12
|
+
@media_asset = MediaAsset.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def create
|
|
16
|
+
@media_asset = MediaAsset.new(media_asset_params)
|
|
17
|
+
file = params.dig(:media_asset, :file)
|
|
18
|
+
|
|
19
|
+
unless file.respond_to?(:read)
|
|
20
|
+
@media_asset.errors.add(:base, "Image file is required.")
|
|
21
|
+
return render :new, status: :unprocessable_entity
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
unless MediaAsset::IMAGE_CONTENT_TYPES.include?(file.content_type.to_s)
|
|
25
|
+
@media_asset.errors.add(:content_type, "must be PNG, JPEG, WebP, GIF, or SVG")
|
|
26
|
+
return render :new, status: :unprocessable_entity
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
upload = Journeybook::Storage::S3Client.new.upload(file)
|
|
30
|
+
@media_asset.assign_attributes(upload.to_h)
|
|
31
|
+
@media_asset.title = upload.original_filename if @media_asset.title.blank?
|
|
32
|
+
|
|
33
|
+
if @media_asset.save
|
|
34
|
+
redirect_to admin_media_assets_path, notice: "Image uploaded."
|
|
35
|
+
else
|
|
36
|
+
Journeybook::Storage::S3Client.new.delete(upload.storage_key)
|
|
37
|
+
render :new, status: :unprocessable_entity
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def destroy
|
|
42
|
+
MediaAsset.find(params[:id]).destroy
|
|
43
|
+
redirect_to admin_media_assets_path, notice: "Image deleted."
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def media_asset_params
|
|
49
|
+
params.fetch(:media_asset, {}).permit(:title, :alt_text, :description, :tags)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def storage_error(error)
|
|
53
|
+
@media_asset ||= MediaAsset.new(media_asset_params)
|
|
54
|
+
@media_asset.errors.add(:base, error.message)
|
|
55
|
+
render :new, status: :unprocessable_entity
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
module Journeybook
|
|
2
|
+
module Admin
|
|
3
|
+
class NavigationItemsController < ApplicationController
|
|
4
|
+
before_action :set_menu
|
|
5
|
+
before_action :set_item, only: %i[edit update destroy move]
|
|
6
|
+
before_action :set_pages, only: %i[new create edit update]
|
|
7
|
+
|
|
8
|
+
def new
|
|
9
|
+
@item = @menu.items.build(position: next_position, visible: true)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def create
|
|
13
|
+
@item = @menu.items.build(item_params)
|
|
14
|
+
|
|
15
|
+
if @item.save
|
|
16
|
+
redirect_to edit_admin_navigation_menu_path(@menu), notice: "Navigation item added."
|
|
17
|
+
else
|
|
18
|
+
render :new, status: :unprocessable_entity
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def edit
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def update
|
|
26
|
+
if @item.update(item_params)
|
|
27
|
+
redirect_to edit_admin_navigation_menu_path(@menu), notice: "Navigation item updated."
|
|
28
|
+
else
|
|
29
|
+
render :edit, status: :unprocessable_entity
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def destroy
|
|
34
|
+
@item.destroy
|
|
35
|
+
redirect_to edit_admin_navigation_menu_path(@menu), notice: "Navigation item deleted."
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def move
|
|
39
|
+
items = @menu.items.ordered.to_a
|
|
40
|
+
index = items.index(@item)
|
|
41
|
+
other_index = params[:direction] == "up" ? index - 1 : index + 1
|
|
42
|
+
other = items[other_index]
|
|
43
|
+
|
|
44
|
+
if other
|
|
45
|
+
NavigationItem.transaction do
|
|
46
|
+
item_position = @item.position
|
|
47
|
+
@item.update!(position: other.position)
|
|
48
|
+
other.update!(position: item_position)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
redirect_to edit_admin_navigation_menu_path(@menu), notice: "Navigation item moved."
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def reorder
|
|
56
|
+
ids = Array(params[:item_ids]).map(&:to_i)
|
|
57
|
+
return head :unprocessable_entity unless ids.sort == @menu.items.pluck(:id).sort
|
|
58
|
+
|
|
59
|
+
NavigationItem.transaction do
|
|
60
|
+
ids.each_with_index { |id, position| @menu.items.find(id).update!(position: position) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
head :no_content
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def set_menu
|
|
69
|
+
@menu = NavigationMenu.find(params[:navigation_menu_id])
|
|
70
|
+
session[:journeybook_site_id] = @menu.site_id
|
|
71
|
+
@current_site = @menu.site
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def set_item
|
|
75
|
+
@item = @menu.items.find(params[:id])
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def set_pages
|
|
79
|
+
@pages = current_site.pages.order(:title)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def item_params
|
|
83
|
+
params.require(:navigation_item).permit(:page_id, :label, :description, :external_url, :visible, :open_in_new_window)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def next_position
|
|
87
|
+
@menu.items.maximum(:position).to_i + 1
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module Journeybook
|
|
2
|
+
module Admin
|
|
3
|
+
class NavigationMenusController < ApplicationController
|
|
4
|
+
before_action :set_menu, only: %i[edit update destroy]
|
|
5
|
+
|
|
6
|
+
def index
|
|
7
|
+
@menus = current_site ? current_site.navigation_menus.ordered : NavigationMenu.none
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def new
|
|
11
|
+
@menu = current_site.navigation_menus.build(position: next_position)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create
|
|
15
|
+
@menu = current_site.navigation_menus.build(menu_params)
|
|
16
|
+
|
|
17
|
+
if @menu.save
|
|
18
|
+
redirect_to edit_admin_navigation_menu_path(@menu), notice: "Navigation menu created."
|
|
19
|
+
else
|
|
20
|
+
render :new, status: :unprocessable_entity
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def edit
|
|
25
|
+
@items = @menu.items.includes(:page).ordered
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def update
|
|
29
|
+
if @menu.update(menu_params)
|
|
30
|
+
redirect_to edit_admin_navigation_menu_path(@menu), notice: "Navigation menu updated."
|
|
31
|
+
else
|
|
32
|
+
@items = @menu.items.includes(:page).ordered
|
|
33
|
+
render :edit, status: :unprocessable_entity
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def destroy
|
|
38
|
+
@menu.destroy
|
|
39
|
+
redirect_to admin_navigation_menus_path, notice: "Navigation menu deleted."
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def set_menu
|
|
45
|
+
@menu = NavigationMenu.find(params[:id])
|
|
46
|
+
session[:journeybook_site_id] = @menu.site_id
|
|
47
|
+
@current_site = @menu.site
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def menu_params
|
|
51
|
+
params.require(:navigation_menu).permit(:key, :title, :position)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def next_position
|
|
55
|
+
current_site.navigation_menus.maximum(:position).to_i + 1
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|