diff options
| -rw-r--r-- | app/assets/stylesheets/components/_buttons.sass | 41 | ||||
| -rw-r--r-- | app/decorators/line_decorator.rb | 18 | ||||
| -rw-r--r-- | app/helpers/table_builder_helper.rb | 13 | ||||
| -rw-r--r-- | app/views/layouts/navigation/_page_header.html.slim | 10 | ||||
| -rw-r--r-- | app/views/lines/show.html.slim | 7 | ||||
| -rw-r--r-- | lib/af83/decorator.rb | 138 | ||||
| -rw-r--r-- | spec/lib/af83/decorator/decorator_spec.rb | 194 | 
7 files changed, 357 insertions, 64 deletions
| diff --git a/app/assets/stylesheets/components/_buttons.sass b/app/assets/stylesheets/components/_buttons.sass index a649a07ef..e70c89733 100644 --- a/app/assets/stylesheets/components/_buttons.sass +++ b/app/assets/stylesheets/components/_buttons.sass @@ -142,16 +142,25 @@ table, .table          margin: 0          border-radius: 0          box-shadow: 0 0 3px rgba($darkgrey, 0.25) - -        > li > a, > li > button -          padding: 5px 15px - -        > li.delete-action -          > a, > button +        > ul +          padding: 0 +          margin: 0 +          > li > a, > li > button +            padding: 5px 15px +            white-space: nowrap +            color: black +            padding: 5px 15px +            font-weight: normal +            line-height: 1.42857              display: block +            font-size: 14px +            &:hover +              text-decoration: none +              color: #262626 +              background-color: whitesmoke +          &:not(:first-child)              position: relative              margin-top: 11px -              &:before                content: ''                display: block @@ -162,14 +171,18 @@ table, .table                height: 1px                background-color: $grey -            .fa:first-child -              margin-right: 0.5em - -          & + li.delete-action +          > li.delete-action              > a, > button -              margin-top: 0 -              &:before -                display: none +              display: block +              position: relative +              .fa:first-child +                margin-right: 0.5em + +            & + li.delete-action +              > a, > button +                margin-top: 0 +                &:before +                  display: none    &.table-2entries .t2e-item diff --git a/app/decorators/line_decorator.rb b/app/decorators/line_decorator.rb index 6c6df1a95..9325c6a8b 100644 --- a/app/decorators/line_decorator.rb +++ b/app/decorators/line_decorator.rb @@ -27,21 +27,21 @@ class LineDecorator < AF83::Decorator    can_edit_line = ->(){ h.policy(Chouette::Line).create? && context[:line_referential].organisations.include?(context[:current_organisation]) }    with_condition can_edit_line do -    action_link on: :index do |l| -      l.content { h.t('lines.actions.new') } -      l.href    { h.new_line_referential_line_path(context[:line_referential]) } -    end - -    action_link on: %i(index show), primary: :show do |l| +    action_link on: %i(index show), primary: :show, secondary: :index do |l|        l.content { h.t('lines.actions.edit') }        l.href    { h.edit_line_referential_line_path(context[:line_referential], object.id) }      end + +    action_link on: :index, secondary: :index do |l| +      l.content { h.t('lines.actions.new') } +      l.href    { h.new_line_referential_line_path(context[:line_referential]) } +    end    end    ### the option :policy will automatically check for the corresponding method    ### on the object's policy -  action_link policy: :deactivate, secondary: true do |l| +  action_link policy: :deactivate, secondary: :show, footer: :index do |l|      l.content  { h.deactivate_link_content('lines.actions.deactivate') }      l.href     { h.deactivate_line_referential_line_path(context[:line_referential], object) }      l.method   :put @@ -49,7 +49,7 @@ class LineDecorator < AF83::Decorator      l.extra_class "delete-action"    end -  action_link policy: :activate, secondary: true do |l| +  action_link policy: :activate, secondary: :show, footer: :index do |l|      l.content  { h.activate_link_content('lines.actions.activate') }      l.href     { h.activate_line_referential_line_path(context[:line_referential], object) }      l.method   :put @@ -57,7 +57,7 @@ class LineDecorator < AF83::Decorator      l.extra_class "delete-action"    end -  action_link policy: :destroy do |l| +  action_link policy: :destroy, footer: true do |l|      l.content  { h.destroy_link_content('lines.actions.destroy') }      l.href     { h.line_referential_line_path(context[:line_referential], object) }      l.method   :delete diff --git a/app/helpers/table_builder_helper.rb b/app/helpers/table_builder_helper.rb index e66e9c942..bece3bb2a 100644 --- a/app/helpers/table_builder_helper.rb +++ b/app/helpers/table_builder_helper.rb @@ -306,12 +306,13 @@ module TableBuilderHelper        content_tag :span, '', class: 'fa fa-cog'      end -    menu = content_tag :ul, class: 'dropdown-menu' do -      ( -        CustomLinks.new(item, pundit_user, links, referential).links + -        item.action_links -      ).map do |link| -        gear_menu_link(link) +    menu = content_tag :div, class: 'dropdown-menu' do +      item.action_links(params[:action]).grouped_by(:primary, :secondary, :footer).map do |group, _links| +        if _links.any? +          content_tag :ul, class: group do +            _links.map{|link| gear_menu_link(link)}.join.html_safe +          end +        end        end.join.html_safe      end diff --git a/app/views/layouts/navigation/_page_header.html.slim b/app/views/layouts/navigation/_page_header.html.slim index 90fd7d855..076c76de4 100644 --- a/app/views/layouts/navigation/_page_header.html.slim +++ b/app/views/layouts/navigation/_page_header.html.slim @@ -19,5 +19,15 @@ div.page_header            - if content_for? :page_header_actions              = yield :page_header_actions +  - action_links = resource.action_links(params[:action]) rescue nil +  - if action_links&.primary&.any? || action_links&.secondary&.any? +    .row +      .col-lg-12.text-right.mb-sm +        - action_links.primary.each do |link| +          = link.to_html do |l| +            - l.class "btn btn-primary #{l.disabled ? "disabled" : ""} sticky-action" +        - action_links.secondary.each do |link| +          = link.to_html do |l| +            - l.class "btn btn-primary #{l.disabled ? "disabled" : ""}"      - if content_for? :page_header_content        = yield :page_header_content diff --git a/app/views/lines/show.html.slim b/app/views/lines/show.html.slim index 1c0578b0b..34b907bdd 100644 --- a/app/views/lines/show.html.slim +++ b/app/views/lines/show.html.slim @@ -1,11 +1,4 @@  - breadcrumb :line, @line -- content_for :page_header_content do -  .row -    .col-lg-12.text-right.mb-sm -      - @line.action_links(:show, :primary).each do |link| -        = link.to_html do |l| -          - l.class "btn btn-primary #{l.disabled ? "disabled" : ""}" -  - page_header_content_for @line  .page_content diff --git a/lib/af83/decorator.rb b/lib/af83/decorator.rb index 50889ede7..6c7dff59d 100644 --- a/lib/af83/decorator.rb +++ b/lib/af83/decorator.rb @@ -24,16 +24,19 @@ class AF83::Decorator < Draper::Decorator      (@_action_links || []).flatten.compact.select{|l| l.for_action?(action)}    end -  def action_links action=:index, scope=nil -    return send("#{scope}_links", action) if scope.present? - -    self.class.action_links(action)\ -    .map{|l| l.bind_to_context(self)}\ -    .select{|l| l.enabled?} +  def action_links action=:index, opts={} +    links = ActionLinks.new links: self.class.action_links(action), context: self, action: action +    group = opts[:group] +    links = links.for_group opts[:group] +    links    end    def primary_links action=:index -    action_links(action).select{|l| l.primary_for_action?(action)} +    action_links(action, group: :primary) +  end + +  def secondary_links action=:index +    action_links(action, group: :secondary)    end    def check_policy policy @@ -45,7 +48,7 @@ class AF83::Decorator < Draper::Decorator    private    def self.parse_options args      options = {} -    %i(weight primary secondary on action actions policy if).each do |k| +    %i(weight primary secondary footer on action actions policy if groups group).each do |k|        options[k] = args.delete(k) if args.has_key?(k)      end      link_options = args.dup @@ -56,14 +59,103 @@ class AF83::Decorator < Draper::Decorator      actions = [actions] unless actions.is_a?(Array)      link_options[:_actions] = actions.compact -    link_options[:_primary] = options.delete :primary -    link_options[:_secondary] = options.delete :secondary +    link_options[:_groups] = options.delete(:groups) +    link_options[:_groups] ||= {} +    if single_group = options.delete(:group) +      if(single_group.is_a?(Symbol) || single_group.is_a?(String)) +        link_options[:_groups][single_group] = true +      else +        link_options[:_groups].update single_group +      end +    end +    link_options[:_groups][:primary] ||= options.delete :primary +    link_options[:_groups][:secondary] ||= options.delete :secondary +    link_options[:_groups][:footer] ||= options.delete :footer      link_options[:_if] = options.delete(:if)      link_options[:_policy] = options.delete(:policy)      [options, link_options]    end +  class ActionLinks +    attr_reader :options + +    def initialize opts +      @options = opts.deep_dup +    end + +    def for_group group +      returning_a_copy do +        @options[:groups] = [group] if group.present? +      end +    end + +    def for_groups groups +      returning_a_copy do +        @options[:groups] = groups if groups.present? +      end +    end + +    def primary +      for_group :primary +    end + +    def secondary +      for_group :secondary +    end + +    def resolve +      out = @options[:links].map{|l| l.bind_to_context(@options[:context])}.select{|l| l.enabled?} +      if @options[:groups].present? +        out = out.select do |l| +          @options[:groups].any? do |g| +            l.in_group_for_action?(@options[:action], g) +          end +        end +      end +      out +    end + +    def grouped_by *groups +      add_footer = groups.include?(:footer) +      groups -= [:footer] +      out = HashWithIndifferentAccess[*groups.map{|g| [g, []]}.flatten(1)] +      out[:other] = [] +      if add_footer +        out[:footer] = [] +        groups << :footer +      end + +      each do |l| +        found = false +        groups.each do |g| +          if l.in_group_for_action?(@options[:action], g) +            out[g] << l +            found = true +            next +          end +        end +        out[:other] << l unless found +      end +      out +    end + +    alias_method :to_ary, :resolve + +    %w(each map size first last any?).each do |meth| +      define_method meth do |*args, &block| +        resolve.send meth, *args, &block +      end +    end + +    private +    def returning_a_copy &block +      out = ActionLinks.new options +      out.instance_eval &block +      out +    end +  end +    class Link      REQUIRED_ATTRIBUTES = %i(href content) @@ -115,21 +207,19 @@ class AF83::Decorator < Draper::Decorator        enabled_actions.empty? || enabled_actions.include?(action.to_s)      end -    %i(primary secondary).each do |k| -      define_method "#{k}_for_action?" do |action| -        vals = send("#{k}_actions") -        if vals.is_a?(Array) -          return vals.include?(action.to_s) -        elsif vals.is_a?(String) || vals.is_a?(Symbol) -          vals.to_s == action.to_s -        else -          !!vals -        end -      end +    def actions_for_group group +      val = @options[:_groups][group] +      val.is_a?(Array) ? val.map(&:to_s) : val +    end -      define_method "#{k}_actions" do -        val = @options[:"_#{k}"] -        val.is_a?(Array) ? val.map(&:to_s) : val +    def in_group_for_action? action, group +      vals = actions_for_group(group) +      if vals.is_a?(Array) +        return vals.include?(action.to_s) +      elsif vals.is_a?(String) || vals.is_a?(Symbol) +        vals.to_s == action.to_s +      else +        !!vals        end      end diff --git a/spec/lib/af83/decorator/decorator_spec.rb b/spec/lib/af83/decorator/decorator_spec.rb index b70e0d7c1..04dc9df09 100644 --- a/spec/lib/af83/decorator/decorator_spec.rb +++ b/spec/lib/af83/decorator/decorator_spec.rb @@ -13,8 +13,8 @@ RSpec.describe AF83::Decorator, type: :decorator do        link_options.each do |k, v|          expect(_link_options[k]).to eq v        end -      expect(_link_options[:_primary]).to eq true -      expect(_link_options[:_secondary]).to eq %i(index show) +      expect(_link_options[:_groups][:primary]).to eq true +      expect(_link_options[:_groups][:secondary]).to eq %i(index show)        expect(_link_options[:_policy]).to eq :blublu      end    end @@ -424,6 +424,24 @@ RSpec.describe AF83::Decorator, type: :decorator do      end    end +  describe '#primary' do +    let(:decorator) do +      Class.new(AF83::Decorator) +    end + +    let(:decorated) do +      obj = create :line +      decorator.decorate(obj) +    end + +    it "should return a new object everytime" do +      actions = decorated.action_links +      primary = actions.primary +      expect(actions.options[:groups]).to be_nil +      expect(primary.options[:groups]).to_not be_nil +    end +  end +    describe(:primary_links) do      let(:decorated) do        obj = create :line @@ -478,7 +496,7 @@ RSpec.describe AF83::Decorator, type: :decorator do          let(:primary){ %i(show edit) }          it "should return the link" do -          links = decorated.action_links(:show, :primary) +          links = decorated.action_links(:show, group: :primary)            expect(links.size).to eq 1          end        end @@ -487,7 +505,7 @@ RSpec.describe AF83::Decorator, type: :decorator do          let(:primary){  %i(index edit) }          it "should not return the link" do -          links = decorated.action_links(:show, :primary) +          links = decorated.action_links(:show, group: :primary)            expect(links.size).to eq 0          end        end @@ -511,4 +529,172 @@ RSpec.describe AF83::Decorator, type: :decorator do        end      end    end + +  describe("in a group") do +    let(:decorated) do +      obj = create :line +      decorator.decorate(obj) +    end + +    context "without links" do +      let(:decorator) do +        Class.new(AF83::Decorator) +      end + +      it "should return no link" do +        links = decorated.action_links +        expect(links.size).to eq 0 +      end +    end + + +    context "with a single link" do +      let(:link_options) do +        { +          href: "/foo/bar/baz", +          content: "Blublu", +          groups: {foo: group} +        } +      end + +      let(:decorator) do +        klass = Class.new(AF83::Decorator) +        klass.action_link link_options +        klass +      end + +      context "always in" do +        let(:group){ true } + +        it "should return the link" do +          links = decorated.action_links(:show, group: :foo) +          expect(links.size).to eq 1 +        end + +        context "define with group" do +          let(:link_options) do +            { +              href: "/foo/bar/baz", +              content: "Blublu", +              group: :foo +            } +          end + +          let(:decorator) do +            klass = Class.new(AF83::Decorator) +            klass.action_link link_options +            klass +          end + +          it "should return the link" do +            links = decorated.action_links(:show, group: :foo) +            expect(links.size).to eq 1 +          end + +          it "should not return the link" do +            links = decorated.action_links(:show, group: :bar) +            expect(links.size).to eq 0 +          end +        end +      end + +      context "primary on this action" do +        let(:group){ :show } + +        it "should return the link" do +          links = decorated.action_links(:show, group: :foo) +          expect(links.size).to eq 1 +        end +      end + +      context "in this action among others" do +        let(:group){ %i(show edit) } + +        it "should return the link" do +          links = decorated.action_links(:show, group: :foo) +          expect(links.size).to eq 1 +        end +      end + +      context "in other actions" do +        let(:group){  %i(index edit) } + +        it "should not return the link" do +          links = decorated.action_links(:show, group: :foo) +          expect(links.size).to eq 0 +        end +      end + +      context "in another action" do +        let(:group){  :index } + +        it "should not return the link" do +          links = decorated.action_links(:show, group: :foo) +          expect(links.size).to eq 0 +        end +      end + +      context "never" do +        let(:group){ nil } + +        it "should not return the link" do +          links = decorated.action_links(:show, group: :foo) +          expect(links.size).to eq 0 +        end +      end +    end + +    describe(:grouped_by) do +      let(:link_options_1) do +        { +          href: "/foo/bar", +          content: "Blublu", +          primary: true +        } +      end + +      let(:link_options_2) do +        { +          href: "/foo/bar/baz", +          content: "Foo", +          groups: {secondary: :show} +        } +      end + +      let(:link_options_3) do +        { +          href: "/foo/bar/baz/bat", +          content: "Foo", +          groups: {foo: :show} +        } +      end + +      let(:link_options_4) do +        { +          href: "/footer", +          content: "Foo", +          footer: true +        } +      end + +      let(:decorator) do +        klass = Class.new(AF83::Decorator) +        klass.action_link link_options_1 +        klass.action_link link_options_2 +        klass.action_link link_options_3 +        klass.action_link link_options_4 +        klass +      end + +      it "should return links in their groups" do +        links = decorated.action_links(:show).grouped_by(:primary, :secondary, :blu, :footer) +        expect(links.size).to eq 5 +        instance_exec links[:primary].first, link_options_1, &link_should_match_options +        instance_exec links[:secondary].first, link_options_2, &link_should_match_options +        expect(links[:blu].size).to eq 0 +        instance_exec links[:other].first, link_options_3, &link_should_match_options +        instance_exec links[:footer].first, link_options_4, &link_should_match_options +      end +    end +  end  end | 
