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 |
