diff --git a/.gitignore b/.gitignore index 404ce1a..d5041ae 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ tmp *.o *.a mkmf.log +.byebug_history \ No newline at end of file diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..2c9b4ef --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +2.7.3 diff --git a/README.md b/README.md index 276b6d3..c7db562 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ rails generate loaf:install * [2.1.1 controller](#211-controller) * [2.1.2 view](#212-view) * [2.1.3 :match](#213-match) + * [2.1.4 :request_methods](#214-request_methods) * [2.2 breadcrumb_trail](#22-breadcrumb_trail) * [3. Configuration](#3-configuration) * [4. Translation](#4-translation) @@ -207,6 +208,33 @@ To make a breadcrumb current based on the query parameters do: breadcrumb "Posts", posts_path(order: :desc), match: {order: :desc} ``` +#### 2.1.4 :request_methods + +**Loaf** allows you to match on multiple HTTP methods in order to make a breadcrumb current with the `:request_methods` option. + +The `:request_methods` key accepts `:all` or an array with following values: + +* `:get` +* `:post` +* `:put` +* `:patch` +* `:delete` +* `:head` +* `:options` +* `:link` +* `:unlink` +* `:trace` + +It's defaults to `%i[get head]` + +Here are some examples: + +```ruby +request_methods: %i[get head] +request_methods: %i[post get head] +request_methods: :all +``` + ### 2.2 breadcrumb_trail In order to display breadcrumbs use the `breadcrumb_trail` view helper. It accepts optional argument of configuration options and can be used in two ways. diff --git a/lib/loaf/configuration.rb b/lib/loaf/configuration.rb index f5a8c0f..a062f53 100644 --- a/lib/loaf/configuration.rb +++ b/lib/loaf/configuration.rb @@ -4,7 +4,8 @@ module Loaf class Configuration VALID_ATTRIBUTES = [ :locales_path, - :match + :match, + :request_methods ].freeze attr_accessor(*VALID_ATTRIBUTES) @@ -13,6 +14,8 @@ class Configuration DEFAULT_MATCH = :inclusive + DEFAULT_REQUEST_METHODS = %i[get head].freeze + # Setup this configuration # # @api public diff --git a/lib/loaf/crumb.rb b/lib/loaf/crumb.rb index 9fe1de1..d950249 100644 --- a/lib/loaf/crumb.rb +++ b/lib/loaf/crumb.rb @@ -10,10 +10,13 @@ class Crumb attr_reader :match + attr_reader :request_methods + def initialize(name, url, options = {}) @name = name || raise_name_error @url = url || raise_url_error @match = options.fetch(:match, Loaf.configuration.match) + @request_methods = options.fetch(:request_methods, Loaf.configuration.request_methods) freeze end diff --git a/lib/loaf/view_extensions.rb b/lib/loaf/view_extensions.rb index bcd158a..d4aa964 100644 --- a/lib/loaf/view_extensions.rb +++ b/lib/loaf/view_extensions.rb @@ -52,7 +52,11 @@ def breadcrumb_trail(options = {}) _breadcrumbs.each do |crumb| name = title_for(crumb.name) path = url_for(_expand_url(crumb.url)) - current = current_crumb?(path, options.fetch(:match) { crumb.match }) + current = current_crumb?( + path, + options.fetch(:match) { crumb.match }, + request_methods: options.fetch(:request_methods) { crumb.request_methods } + ) yield(Loaf::Breadcrumb[name, path, current]) end @@ -65,8 +69,8 @@ def breadcrumb_trail(options = {}) # the pattern to match on # # @api public - def current_crumb?(path, pattern = :inclusive) - return false unless request.get? || request.head? + def current_crumb?(path, pattern = :inclusive, request_methods: Loaf.configuration.request_methods) + return false unless match_request_methods(request_methods) origin_path = URI::DEFAULT_PARSER.unescape(path).force_encoding(Encoding::BINARY) @@ -128,5 +132,16 @@ def _expand_url(url) url end end + + # Check if the HTTP request methods are allowed + # + # @retun [Boolean] + # + # @api private + def match_request_methods(request_methods) + return true if request_methods == :all + + request_methods.any? { |method| request.try("#{method}?") } + end end # ViewExtensions end # Loaf diff --git a/spec/integration/breadcrumb_trail_spec.rb b/spec/integration/breadcrumb_trail_spec.rb index bda6c72..0b8d248 100644 --- a/spec/integration/breadcrumb_trail_spec.rb +++ b/spec/integration/breadcrumb_trail_spec.rb @@ -8,66 +8,116 @@ it "shows root breadcrumb" do visit root_path - page.within '#breadcrumbs .selected' do - expect(page.html).to include('Home') + within "#breadcrumbs .selected" do + expect(page).to have_link(href: "/", text: "Home") end end it "inherits controller breadcrumb and adds index action breadcrumb" do visit posts_path - page.within '#breadcrumbs' do - expect(page.html).to include('Home') + within "#breadcrumbs" do + expect(page).to have_link(href: "/", text: "Home") end - page.within '#breadcrumbs .selected' do - expect(page.html).to include('All Posts') + within "#breadcrumbs .selected" do + expect(page).to have_link(href: "/posts", text: "All Posts") end end - it 'filters out controller breadcrumb and adds new action breadcrumb' do + it "filters out controller breadcrumb and adds new action breadcrumb" do visit new_post_path - page.within '#breadcrumbs' do - expect(page).to_not have_content('Home') - expect(page).to have_content('New Post') + within "#breadcrumbs" do + expect(page).to_not have_content("Home") + expect(page).to have_content("New Post") end end it "adds breadcrumb in view with path variable" do visit post_path(1) - page.within '#breadcrumbs .selected' do - expect(page.html).to include('Show Post in view') + within "#breadcrumbs .selected" do + expect(page).to have_link(href: "/posts/1", text: "Show Post in view") end end - it 'is current when forced' do + it "is current when forced" do visit new_post_path expect(page.current_path).to eq(new_post_path) - page.within '#breadcrumbs' do - expect(page).to have_selector('li.selected', count: 2) - expect(page.html).to include('All') - expect(page.html).to include('New Post') + within "#breadcrumbs" do + expect(page).to have_selector("li.selected", count: 2) + expect(page).to have_link(href: "/posts", text: "All") + expect(page).to have_link(href: "/posts/new", text: "New Post") end end it "allows for procs in name and url" do visit post_comments_path(1) - page.within '#breadcrumbs .selected' do - expect(page.html).to include('Post comments') + within "#breadcrumbs .selected" do + expect(page).to have_link(href: "/posts/1/comments", text: "Post comments") end end it "allows for procs in name and url without supplying the controller" do - visit post_comments_path(1) + visit post_comments_path(1, no_controller: true) - page.within "#breadcrumbs .selected" do - expect(page.html).to include( - ''\ - "Post comments No Controller" + within "#breadcrumbs .selected" do + expect(page).to have_link( + href: "/posts/1/comments?no_controller=true", + text: "Post comments No Controller" ) end end + + it "match to Non-GET methods" do + visit onboard_path + + expect(page).to have_selector("h1", text: "Onboard") + within "#breadcrumbs" do + expect(page).to have_content("Onboard") + end + + click_link "Step 1" # GET + expect(page).to have_selector("h1", text: "Step 1") + within "#breadcrumbs" do + expect(page).to have_link(href: "/onboard", text: "Onboard") + end + within "#breadcrumbs .selected" do + expect(page).to have_link(href: "/onboard/step/1", text: "Step 1") + end + + click_on "Save & Next" # POST + expect(page).to have_selector("h1", text: "Step 2") + within "#breadcrumbs .selected" do + expect(page).to have_link(href: "/onboard/step/2", text: "Step 2") + end + + click_on "Save & Next" # PUT + expect(page).to have_selector("h1", text: "Step 3") + within "#breadcrumbs .selected" do + expect(page).to have_link(href: "/onboard/step/3", text: "Step 3") + end + + if Rails.version >= "4.0.0" + click_on "Save & Next" # PATCH + expect(page).to have_selector("h1", text: "Step 4") + within "#breadcrumbs .selected" do + expect(page).to have_link(href: "/onboard/step/4", text: "Step 4") + end + end + + click_on "Save & Next" # DELETE + expect(page).to have_selector("h1", text: "Step 5") + within "#breadcrumbs .selected" do + expect(page).to have_link(href: "/onboard/step/5", text: "Step 5") + end + + click_on "Save & Next" # GET + expect(page).to have_selector("h1", text: "Step 6") + within "#breadcrumbs .selected" do + expect(page).to have_link(href: "/onboard/step/6", text: "Step 6") + end + end end diff --git a/spec/integration/configuration_spec.rb b/spec/integration/configuration_spec.rb index 4aae487..b6ff5db 100644 --- a/spec/integration/configuration_spec.rb +++ b/spec/integration/configuration_spec.rb @@ -1,10 +1,10 @@ # encoding: utf-8 -RSpec.describe 'setting configuration options' do +RSpec.describe "setting configuration options" do it "contains 'selected' inside the breadcrumb markup" do visit posts_path - page.within '#breadcrumbs' do - expect(page).to have_selector('.selected') + within "#breadcrumbs" do + expect(page).to have_selector(".selected") end end end diff --git a/spec/rails_app/app/controllers/comments_controller.rb b/spec/rails_app/app/controllers/comments_controller.rb index c97a999..1375565 100644 --- a/spec/rails_app/app/controllers/comments_controller.rb +++ b/spec/rails_app/app/controllers/comments_controller.rb @@ -3,10 +3,12 @@ class Article < Struct.new(:id, :title); end class CommentsController < ApplicationController breadcrumb lambda { |c| c.find_article(c.params[:post_id]).title }, - lambda { |c| c.post_comments_path(c.params[:post_id]) } + lambda { |c| c.post_comments_path(c.params[:post_id]) }, + match: :exact breadcrumb -> { find_article(params[:post_id]).title + " No Controller" }, - -> { post_comments_path(params[:post_id], no_controller: true) } + -> { post_comments_path(params[:post_id], no_controller: true) }, + match: :exact def index end diff --git a/spec/rails_app/app/controllers/onboard_controller.rb b/spec/rails_app/app/controllers/onboard_controller.rb new file mode 100644 index 0000000..f5266bb --- /dev/null +++ b/spec/rails_app/app/controllers/onboard_controller.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class OnboardController < ApplicationController + breadcrumb "Onboard", :onboard_path, match: :exact + + def setup + case params[:step] + when "1" + breadcrumb "Step 1", onboard_step_path(step: 1), + match: :exact, request_methods: %i[get] + render :step1 + when "2" + breadcrumb "Step 2", onboard_step_path(step: 2), + match: :exact, request_methods: %i[get post] + render :step2 + when "3" + breadcrumb "Step 3", onboard_step_path(step: 3), + match: :exact, request_methods: %i[get put] + render :step3 + when "4" + breadcrumb "Step 4", onboard_step_path(step: 4), + match: :exact, request_methods: %i[get patch] + render :step4 + when "5" + breadcrumb "Step 5", onboard_step_path(step: 5), + match: :exact, request_methods: %i[get delete] + render :step5 + when "6" + breadcrumb "Step 6", onboard_step_path(step: 6), + match: :exact, request_methods: :all + render :step6 + else + render :setup + end + end +end \ No newline at end of file diff --git a/spec/rails_app/app/views/onboard/setup.html.erb b/spec/rails_app/app/views/onboard/setup.html.erb new file mode 100644 index 0000000..5b4fea4 --- /dev/null +++ b/spec/rails_app/app/views/onboard/setup.html.erb @@ -0,0 +1,3 @@ +

Onboard

+ +<%= link_to "Step 1", onboard_step_path(step: 1) %> \ No newline at end of file diff --git a/spec/rails_app/app/views/onboard/step1.html.erb b/spec/rails_app/app/views/onboard/step1.html.erb new file mode 100644 index 0000000..0052960 --- /dev/null +++ b/spec/rails_app/app/views/onboard/step1.html.erb @@ -0,0 +1,2 @@ +

Step 1

+<%= button_to "Save & Next", onboard_step_path(step: 2), method: :post %> \ No newline at end of file diff --git a/spec/rails_app/app/views/onboard/step2.html.erb b/spec/rails_app/app/views/onboard/step2.html.erb new file mode 100644 index 0000000..0ea7e80 --- /dev/null +++ b/spec/rails_app/app/views/onboard/step2.html.erb @@ -0,0 +1,2 @@ +

Step 2

+<%= button_to "Save & Next", onboard_step_path(step: 3), method: :put %> \ No newline at end of file diff --git a/spec/rails_app/app/views/onboard/step3.html.erb b/spec/rails_app/app/views/onboard/step3.html.erb new file mode 100644 index 0000000..1d9b7aa --- /dev/null +++ b/spec/rails_app/app/views/onboard/step3.html.erb @@ -0,0 +1,6 @@ +

Step 3

+<% if Rails.version >= "4.0.0" %> + <%= button_to "Save & Next", onboard_step_path(step: 4), method: :patch %> +<% else %> + <%= button_to "Save & Next", onboard_step_path(step: 5), method: :delete %> +<% end %> \ No newline at end of file diff --git a/spec/rails_app/app/views/onboard/step4.html.erb b/spec/rails_app/app/views/onboard/step4.html.erb new file mode 100644 index 0000000..e425a0d --- /dev/null +++ b/spec/rails_app/app/views/onboard/step4.html.erb @@ -0,0 +1,2 @@ +

Step 4

+<%= button_to "Save & Next", onboard_step_path(step: 5), method: :delete %> \ No newline at end of file diff --git a/spec/rails_app/app/views/onboard/step5.html.erb b/spec/rails_app/app/views/onboard/step5.html.erb new file mode 100644 index 0000000..a28f358 --- /dev/null +++ b/spec/rails_app/app/views/onboard/step5.html.erb @@ -0,0 +1,2 @@ +

Step 5

+<%= button_to "Save & Next", onboard_step_path(step: 6), method: :get %> \ No newline at end of file diff --git a/spec/rails_app/app/views/onboard/step6.html.erb b/spec/rails_app/app/views/onboard/step6.html.erb new file mode 100644 index 0000000..0f72669 --- /dev/null +++ b/spec/rails_app/app/views/onboard/step6.html.erb @@ -0,0 +1 @@ +

Step 6

\ No newline at end of file diff --git a/spec/rails_app/config/routes.rb b/spec/rails_app/config/routes.rb index ff293a3..30b0efc 100644 --- a/spec/rails_app/config/routes.rb +++ b/spec/rails_app/config/routes.rb @@ -1,7 +1,16 @@ RailsApp::Application.routes.draw do - root :to => 'home#index' + root to: "home#index" resources :posts do resources :comments end + + get "/onboard", to: "onboard#setup", as: :onboard + get "/onboard/step/:step", to: "onboard#setup", as: :onboard_step + post "/onboard/step/:step", to: "onboard#setup" + if Rails.version >= "4.0.0" + patch "/onboard/step/:step", to: "onboard#setup" + end + put "/onboard/step/:step", to: "onboard#setup" + delete "/onboard/step/:step", to: "onboard#setup" end diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 1ffbe95..21ed616 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -3,6 +3,7 @@ RSpec.configure do |c| c.include Capybara::DSL, :file_path => /\bspec\/integration\// + c.filter_run_when_matching focus: true end Capybara.default_driver = :rack_test Capybara.default_selector = :css diff --git a/spec/support/dummy_view.rb b/spec/support/dummy_view.rb index 3632830..d33a175 100644 --- a/spec/support/dummy_view.rb +++ b/spec/support/dummy_view.rb @@ -3,9 +3,45 @@ class DummyView < ActionView::Base module FakeRequest class Request - attr_accessor :path, :fullpath, :protocol, :host_with_port + attr_accessor :path, + :fullpath, + :protocol, + :host_with_port, + :_request_method def get? - true + return true if _request_method.nil? + + _request_method == "GET" + end + + def post? + return false if _request_method.nil? + + _request_method == "POST" + end + + def put? + return false if _request_method.nil? + + _request_method == "PUT" + end + + def patch? + return false if _request_method.nil? + + _request_method == "PATCH" + end + + def delete? + return false if _request_method.nil? + + _request_method == "DELETE" + end + + def head? + return false if _request_method.nil? + + _request_method == "HEAD" end end def request @@ -51,4 +87,8 @@ def set_path(path) request.protocol = "http://" request.host_with_port = "www.example.com" end + + def set_request_method(method) + request._request_method = method.upcase.to_s + end end diff --git a/spec/unit/configuration_spec.rb b/spec/unit/configuration_spec.rb index b90bfde..ed1bb3d 100644 --- a/spec/unit/configuration_spec.rb +++ b/spec/unit/configuration_spec.rb @@ -19,7 +19,8 @@ config = Loaf::Configuration.new expect(config.to_hash).to eq({ locales_path: "/", - match: :inclusive + match: :inclusive, + request_methods: %i[get head] }) end diff --git a/spec/unit/view_extensions/breadcrumb_trail_spec.rb b/spec/unit/view_extensions/breadcrumb_trail_spec.rb index 11658bb..c091690 100644 --- a/spec/unit/view_extensions/breadcrumb_trail_spec.rb +++ b/spec/unit/view_extensions/breadcrumb_trail_spec.rb @@ -280,4 +280,34 @@ expect(view.breadcrumb_trail.map(&:url)).to eq(%w[/ /posts /posts/1]) expect(view.breadcrumb_trail(match: :exact).map(&:current?)).to eq([false, false, true]) end + + it "matches the current path with :request_methods set to custom" do + view = DummyView.new + view.breadcrumb("posts", "/posts", request_methods: %i[get post]) + view.set_path("/posts") + view.set_request_method(:post) + + trail = view.breadcrumb_trail.map(&:to_a) + expect(trail).to eq([["posts", "/posts", true]]) + end + + it "fails to match current path with :request_methods set to default" do + view = DummyView.new + view.breadcrumb("posts", "/posts") + view.set_path("/posts") + view.set_request_method(:post) + + trail = view.breadcrumb_trail.map(&:to_a) + expect(trail).to eq([["posts", "/posts", false]]) + end + + it "matches current path with :request_methods set to :all" do + view = DummyView.new + view.breadcrumb("posts", "/posts", request_methods: :all) + view.set_path("/posts") + view.set_request_method(:delete) + + trail = view.breadcrumb_trail.map(&:to_a) + expect(trail).to eq([["posts", "/posts", true]]) + end end