diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/af83/decorator.rb | 122 | ||||
| -rw-r--r-- | lib/af83/decorator/enhanced_decorator.rb | 145 | ||||
| -rw-r--r-- | lib/af83/decorator/link.rb | 153 | 
3 files changed, 420 insertions, 0 deletions
diff --git a/lib/af83/decorator.rb b/lib/af83/decorator.rb new file mode 100644 index 000000000..f990555fe --- /dev/null +++ b/lib/af83/decorator.rb @@ -0,0 +1,122 @@ +class AF83::Decorator < ModelDecorator +  include AF83::Decorator::EnhancedDecorator +  extend AF83::Decorator::EnhancedDecorator::ClassMethods + +  def self.decorates klass +    instance_decorator.decorates klass +  end + +  def self.instance_decorator +    @instance_decorator ||= begin +      klass = Class.new(AF83::Decorator::InstanceDecorator) +      klass.delegate_all +      klass +    end +  end + +  def self.with_instance_decorator +    @_with_instance_decorator = true +    yield instance_decorator +    @_with_instance_decorator = false +  end + +  def self.decorate object, options = {} +    if object.is_a?(ActiveRecord::Base) +      return instance_decorator.decorate object, options +    else +      self.new object, options.update(with: instance_decorator) +    end +  end + +  def self.define_instance_method method_name, &block +    instance_decorator.send(:define_method, method_name, &block) +  end + +  # Defines a class method on the decorated object's class. These +  # can be called like `object.class.my_method`. +  def self.define_instance_class_method method_name, &block +    instance_decorator.send(:define_singleton_method, method_name, &block) +  end + +  class ActionLinks +    attr_reader :options + +    delegate :each, :map, :size, :first, :last, :any?, :select, to: :resolve + +    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], @options[:action])}.select{|l| l.enabled?} +      if @options[:groups].present? +        out = out.select do |l| +          @options[:groups].any? do |g| +            l.in_group_for_action?(g) +          end +        end +      end +      out +    end +    alias_method :to_ary, :resolve + +    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?(g) +            out[g] << l +            found = true +            next +          end +        end +        out[:other] << l unless found +      end +      out +    end + +    private +    def returning_a_copy &block +      out = ActionLinks.new options +      out.instance_eval &block +      out +    end +  end + +  class IncompleteLinkDefinition < RuntimeError +  end + +  class InstanceDecorator < Draper::Decorator +    include AF83::Decorator::EnhancedDecorator +    extend AF83::Decorator::EnhancedDecorator::ClassMethods +  end +end diff --git a/lib/af83/decorator/enhanced_decorator.rb b/lib/af83/decorator/enhanced_decorator.rb new file mode 100644 index 000000000..904d1b2da --- /dev/null +++ b/lib/af83/decorator/enhanced_decorator.rb @@ -0,0 +1,145 @@ +module AF83::Decorator::EnhancedDecorator +  module ClassMethods +    def action_link args={} +      raise "You are using `action_link` inside a with_instance_decorator block, but not on the instance decorator itself.\n Use `instance_decorator.action_link` or move outside of the block, as this may lead to an unforeseen behaviour." if @_with_instance_decorator +      args[:if] = @_condition if args[:if].nil? + +      options, link_options = parse_options args + +      link = AF83::Decorator::Link.new(link_options) +      instance_exec(link, &options[:before_block]) if options[:before_block] +      yield link if block_given? +      raise AF83::Decorator::IncompleteLinkDefinition.new(link.errors) unless link.complete? + +      weight = options[:weight] || 1 +      @_action_links ||= [] +      @_action_links[weight] ||= [] +      @_action_links[weight] << link +    end + +    ### Here we define some shortcuts that match dthe default behaviours +    def create_action_link args={}, &block +      opts = { +        on: :index, +        primary: :index, +        policy: :create, +        before_block: -> (l){ +          l.content { h.t('actions.add') } +          l.href    { [:new, object.klass.name.underscore.singularize] } +        } +      } +      action_link opts.update(args), &block +    end + +    def show_action_link args={}, &block +      opts = { +        on: :index, +        primary: :index, +        before_block: -> (l){ +          l.content { h.t('actions.show') } +          l.href { [object] } +        } +      } +      action_link opts.update(args), &block +    end + +    def edit_action_link args={}, &block +      opts = { +        primary: %i(show index), +        policy: :edit, +        before_block: -> (l){ +          l.content { h.t('actions.edit') } +          l.href { [:edit, object] } +        } +      } +      action_link opts.update(args), &block +    end + +    def destroy_action_link args={}, &block +      opts = { +        policy: :destroy, +        footer: true, +        secondary: :show, +        before_block: -> (l){ +          l.content { h.destroy_link_content } +          l.href { [object] } +          l.method :delete +          l.data {{ confirm: h.t('actions.destroy_confirm') }} +        } +      } +      action_link opts.update(args), &block +    end + +    def t key +      eval  "-> (l){ h.t('#{key}') }" +    end + +    def with_condition condition, &block +      @_condition = condition +      instance_eval &block +      @_condition = nil +    end + +    def action_links action +      (@_action_links || []).flatten.compact.select{|l| l.for_action?(action)} +    end + +    def parse_options args +      options = {} +      %i(weight primary secondary footer on action actions policy feature if groups group before_block).each do |k| +        options[k] = args.delete(k) if args.has_key?(k) +      end +      link_options = args.dup + +      actions = options.delete :actions +      actions ||= options.delete :on +      actions ||= [options.delete(:action)] +      actions = [actions] unless actions.is_a?(Array) +      link_options[:_actions] = actions.compact + +      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) +      link_options[:_feature] = options.delete(:feature) +      [options, link_options] +    end +  end + +  def action_links action=:index, opts={} +    @action = action&.to_sym +    links = AF83::Decorator::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, group: :primary) +  end + +  def secondary_links action=:index +    action_links(action, group: :secondary) +  end + +  def check_policy policy +    _object = policy.to_s == "create" ? object.klass : object +    method = "#{policy}?" +    h.policy(_object).send(method) +  end + +  def check_feature feature +    h.has_feature? feature +  end +end diff --git a/lib/af83/decorator/link.rb b/lib/af83/decorator/link.rb new file mode 100644 index 000000000..7d2896e6a --- /dev/null +++ b/lib/af83/decorator/link.rb @@ -0,0 +1,153 @@ +class AF83::Decorator::Link +  REQUIRED_ATTRIBUTES = %i(href content) + +  attr_reader :context +  attr_reader :action + +  def initialize options={} +    @options = {} +    options.each do |k, v| +      send "#{k}", v +    end +  end + +  def bind_to_context context, action +    @context = context +    @action = action +    self +  end + +  def method *args +    link_method *args +  end + +  def class *args +    link_class args +  end + +  def method_missing name, *args, &block +    if block_given? +      @options[name] = block +    elsif args.size == 0 +      out = @options[name] +      out = context.instance_exec(self, &out)  if out.is_a?(Proc) +      out +    else +      # we can use l.foo("bar") or l.foo = "bar" +      if name.to_s =~ /\=$/ +        _name = name.to_s.gsub(/=$/, '') +        return send(_name, *args, &block) +      end +      @options[name] = args.first +    end +  end + +  def options +    @options.symbolize_keys +  end + +  def complete? +    @missing_attributes = REQUIRED_ATTRIBUTES.select{|a| !@options[a].present?} +    @missing_attributes.empty? +  end + +  def enabled_actions +    @options[:_actions].map(&:to_s) || [] +  end + +  def for_action? action=nil +    action ||= @action +    enabled_actions.empty? || enabled_actions.include?(action.to_s) +  end + +  def actions_for_group group +    val = @options[:_groups][group] +    val.is_a?(Array) ? val.map(&:to_s) : val +  end + +  def in_group_for_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 + +  def primary? +    in_group_for_action? :primary +  end + +  def secondary? +    in_group_for_action? :secondary +  end + +  def enabled? +    enabled = false +    if @options[:_if].nil? +      enabled = true +    elsif @options[:_if].is_a?(Proc) +      enabled = context.instance_exec(&@options[:_if]) +    else +      enabled = !!@options[:_if] +    end + +    enabled = enabled && check_policy(@options[:_policy]) if @options[:_policy].present? +    enabled = enabled && check_feature(@options[:_feature]) if @options[:_feature].present? +    enabled +  end + +  def check_policy(policy) +    @context.check_policy policy +  end + +  def check_feature(feature) +    @context.check_feature feature +  end + +  def errors +    "Missing attributes: #{@missing_attributes.to_sentence}" +  end + +  def add_class val +    @options[:link_class] ||= [] +    @options[:link_class] << val +    @options[:link_class].flatten! +  end + +  def extra_class +    (options[:link_class] || []).join(' ') +  end + +  def html_options +    out = {} +    options.each do |k, v| +      out[k] = self.send(k) unless k == :content || k == :href || k.to_s =~ /^_/ +    end +    out[:method] = link_method +    out[:class] = extra_class +    out.delete(:link_class) +    out[:class] += " disabled" if disabled +    out[:disabled] = !!disabled +    out +  end + +  def to_html +    if block_given? +      link = AF83::Decorator::Link.new(@options).bind_to_context(context, @action) +      yield link +      return link.to_html +    end +    if type&.to_sym == :button +      HTMLElement.new( +        :button, +        content, +        html_options +      ).to_html +    else +      context.h.link_to content, href, html_options +    end +  end +end  | 
